feat(controller): migrate wifi drivers from tcp to websocket clients
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
|||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
# led-driver/src is MicroPython source — never keep host __pycache__ there (see .cursor/rules/led-driver.mdc)
|
||||||
|
led-driver/src/__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
*.so
|
*.so
|
||||||
|
|||||||
1
Pipfile
1
Pipfile
@@ -13,6 +13,7 @@ requests = "*"
|
|||||||
selenium = "*"
|
selenium = "*"
|
||||||
adafruit-ampy = "*"
|
adafruit-ampy = "*"
|
||||||
microdot = "*"
|
microdot = "*"
|
||||||
|
websockets = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
|
|||||||
711
Pipfile.lock
generated
711
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "6cec0fe6dec67c9177363a558131f333153b6caa47e1ddeca303cb0d19954cf8"
|
"sha256": "18691f772c7660e4a087c90560c87a9217a09e9b6db97825d21c092a06d64b89"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -42,112 +42,112 @@
|
|||||||
},
|
},
|
||||||
"bitarray": {
|
"bitarray": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:004d518fa410e6da43386d20e07b576a41eb417ac67abf9f30fa75e125697199",
|
"sha256:0256d57e294414bfe4fec4f852fd1d9ae361228c71b082332bf81c8b8fc69f80",
|
||||||
"sha256:014df8a9430276862392ac5d471697de042367996c49f32d0008585d2c60755a",
|
"sha256:03fe327549f177040b32f7faa736dc152be936d8b264d8b84f94c75f1379bfa1",
|
||||||
"sha256:01c5f0dc080b0ebb432f7a68ee1e88a76bd34f6d89c9568fcec65fb16ed71f0e",
|
"sha256:07626f76a248fce5ebbb10fb0d4899d3c7f908ba21cb2fb4f5a7a9daf24c20cd",
|
||||||
"sha256:025d133bf4ca8cf75f904eeb8ea946228d7c043231866143f31946a6f4dd0bf3",
|
"sha256:0793c51d3b1c7410bde1f7254fff71fabff1bc0cdeba1fa51319ac4e7931df3d",
|
||||||
"sha256:0ca70ccf789446a6dfde40b482ec21d28067172cd1f8efd50d5548159fccad9e",
|
"sha256:0a33f8931ac91ebc23ce4decb99ed8fdddba2bafd2af3bb2781bcfd9878d4822",
|
||||||
"sha256:0df69d26f21a9d2f1b20266f6737fa43f08aa5015c99900fb69f255fbe4dabb4",
|
"sha256:0a661f3492462e7adf8a054fb7414a22fc8251f1e18b9d8cbcf008d2dc85f012",
|
||||||
"sha256:0f8069a807a3e6e3c361ce302ece4bf1c3b49962c1726d1d56587e8f48682861",
|
"sha256:0c8c66f5d8055cb84ad0ea14af57b3579cb0b6db589f2086f5e33f0922cf2354",
|
||||||
"sha256:15a2eff91f54d2b1f573cca8ca6fb58763ce8fea80e7899ab028f3987ef71cd5",
|
"sha256:0da5f17bed67ffe1d72f79fbf98403513a6e51a4f9b8293c1ff8a64e121242be",
|
||||||
"sha256:15e8d0597cc6e8496de6f4dea2a6880c57e1251502a7072f5631108a1aa28521",
|
"sha256:0f099a4a77daf9bb99787070854894fe588c7d6988ea729f970ba2b3b82c7559",
|
||||||
"sha256:165052a0e61c880f7093808a0c524ce1b3555bfa114c0dfb5c809cd07918a60d",
|
"sha256:10c0caabff00ab0631d1e4fd25f56c7a5cf0f068426e5860d28dbbb972b509bf",
|
||||||
"sha256:178c5a4c7fdfb5cd79e372ae7f675390e670f3732e5bc68d327e01a5b3ff8d55",
|
"sha256:133648c3405564e7fef9103f1768cb018de1b4976f3d8beff09cd4acea73bfe4",
|
||||||
"sha256:18214bac86341f1cc413772e66447d6cca10981e2880b70ecaf4e826c04f95e9",
|
"sha256:154a19e1dcd430494fdad7d1a0fb36383baaa363e1cb9d5a7b744cd2418c44d2",
|
||||||
"sha256:1a54d7e7999735faacdcbe8128e30207abc2caf9f9fd7102d180b32f1b78bfce",
|
"sha256:18f3a2c8908e63a66d3994808254397a5f989b1fb91087c33739f62bf1a1a064",
|
||||||
"sha256:1a926fa554870642607fd10e66ee25b75fdd9a7ca4bbffa93d424e4ae2bf734a",
|
"sha256:190a3482818d69faef176171c7cae10d55cb4dd0c686b5aced7f592b5e5591c1",
|
||||||
"sha256:1c8f2a5d8006db5a555e06f9437e76bf52537d3dfd130cb8ae2b30866aca32c9",
|
"sha256:190b20cbffc9cd7f308f7a57d406119c3af3ae197613325fd2d92d99c8882ad6",
|
||||||
"sha256:21ca6a47bf20db9e7ad74ca04b3d479e4d76109b68333eb23535553d2705339e",
|
"sha256:1d7b786a1ddd9b8dda17c445060a94a465cba2e113603ae7bdc5364efc1efd11",
|
||||||
"sha256:22c540ed20167d3dbb1e2d868ca935180247d620c40eace90efa774504a40e3b",
|
"sha256:20e412527ec1aac7e3a6542b32a9c34bb852c954676b05008f0e3d58c390a0ac",
|
||||||
"sha256:239578587b9c29469ab61149dda40a2fe714a6a4eca0f8ff9ea9439ec4b7bc30",
|
"sha256:21add0aa968496a2bd8341d85720d09808e22e0adc7dbefc1e0f8f67c4b83f36",
|
||||||
"sha256:25b9cff6c9856bc396232e2f609ea0c5ec1a8a24c500cee4cca96ba8a3cd50b6",
|
"sha256:2762db8049b230520358ac742cbc57bceaacebe34e5d25c096f2b4bc3887a3a8",
|
||||||
"sha256:26714898eb0d847aac8af94c4441c9cb50387847d0fe6b9fc4217c086cd68b80",
|
"sha256:29c8c10a49d6a9586f592116618b99c3dabcb24d881b7a649e0691ef87f314c4",
|
||||||
"sha256:28a85b056c0eb7f5d864c0ceef07034117e8ebfca756f50648c71950a568ba11",
|
"sha256:2b9916867fa1ed815739e3e37dda458f397dee25a0e293b808839cfc2a396ca0",
|
||||||
"sha256:2a3d1b05ffdd3e95687942ae7b13c63689f85d3f15c39b33329e3cb9ce6c015f",
|
"sha256:2da2ca9495668ab77132a911f6bd530d2bfe686d10467584894efc3b66e9ffb5",
|
||||||
"sha256:2c3bb96b6026643ce24677650889b09073f60b9860a71765f843c99f9ab38b25",
|
"sha256:2dc07dab252c63c4f6600e200b26fa05207db6b650d41ae88ab0cec4d6c59459",
|
||||||
"sha256:2fa23fdb3beab313950bbb49674e8a161e61449332d3997089fe3944953f1b77",
|
"sha256:300e3026d17ae3328320ba78d3165bdb1c43d0dfdbc461a69ebbdc005d9ce0b3",
|
||||||
"sha256:2fe8c54b15a9cd4f93bc2aaceab354ec65af93370aa1496ba2f9c537a4855ee0",
|
"sha256:30d42c34da2974a5e2e0b51c57ecf89892c1e83ed67e1084d1e27eefc27add91",
|
||||||
"sha256:30989a2451b693c3f9359d91098a744992b5431a0be4858f1fdf0ec76b457125",
|
"sha256:329b994944993c45c3845047476ef4f231fe1a53972f18f8d005fd12fac163e1",
|
||||||
"sha256:31a4ad2b730128e273f1c22300da3e3631f125703e4fee0ac44d385abfb15671",
|
"sha256:3a5e594b4be2dbfe021cee8d6d7d96e9bb19dee7ed7be351f43bca7a0619b978",
|
||||||
"sha256:337c8cd46a4c6568d367ed676cbf2d7de16f890bb31dbb54c44c1d6bb6d4a1de",
|
"sha256:3b9358f6437a5fa0c765ffae5810c9830547baf4bcf469438b82845c3f33f998",
|
||||||
"sha256:33af25c4ff7723363cb8404dfc2eefeab4110b654f6c98d26aba8a08c745d860",
|
"sha256:3b9790ae107fc8648155f120e80a58ef8e94424efefff5b355de84061de6a18b",
|
||||||
"sha256:3ea52df96566457735314794422274bd1962066bfb609e7eea9113d70cf04ffe",
|
"sha256:40d1b57012bf9b4fefd25345aaa95aab3ca510cc693f33c2cb02a4b771d8e51a",
|
||||||
"sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d",
|
"sha256:430fe5150816445c8294a36ce2612360037342d750cea179efe5de38c66670a8",
|
||||||
"sha256:41b53711f89008ba2de62e4c2d2260a8b357072fd4f18e1351b28955db2719dc",
|
"sha256:4494c599effa16064f2b600f6eb28115182d6826847d795a55691339788d8a4d",
|
||||||
"sha256:451f9958850ea98440d542278368c8d1e1ea821e2494b204570ba34a340759df",
|
"sha256:478b9f0ea86f957624dd2b159066855716f78db94666e9b04babe85fc013e01b",
|
||||||
"sha256:46cf239856b87fe1c86dfbb3d459d840a8b1649e7922b1e0bfb6b6464692644a",
|
"sha256:4b7d7d10a1c82050efbb9a83d7a43974f70cf8f021afb86463b42e4ac4e5a46b",
|
||||||
"sha256:4779f356083c62e29b4198d290b7b17a39a69702d150678b7efff0fdddf494a8",
|
"sha256:4c7ce072191ba23a4a4876452ccd5f2a67b926e66a248d052d39e9969cd3ab47",
|
||||||
"sha256:4902f4ecd5fcb6a5f482d7b0ae1c16c21f26fc5279b3b6127363d13ad8e7a9d9",
|
"sha256:4da256fc567a57ded2a4aa962fc9e9d430ab740e5c67be9e98a63ef4eb467f2f",
|
||||||
"sha256:4d73d4948dcc5591d880db8933004e01f1dd2296df9de815354d53469beb26fe",
|
"sha256:4e34f1cb6cdb036c5f4a839a2b74419f75fa36177a70c4bab2867f48973cbe44",
|
||||||
"sha256:4d9984017314da772f5f7460add7a0301a4ffc06c72c2998bb16c300a6253607",
|
"sha256:4fb869faf4b484cb213199ced1e2732091559107637d429fc25d0a9731f5f630",
|
||||||
"sha256:4f298daaaea58d45e245a132d6d2bdfb6f856da50dc03d75ebb761439fb626cf",
|
"sha256:539880ddf9a8cc54c9e6126e7d072c991563f0c90ef73b3519a783d53df00352",
|
||||||
"sha256:50ddbe3a7b4b6ab96812f5a4d570f401a2cdb95642fd04c062f98939610bbeee",
|
"sha256:55f4b105a1686eb486069a9e578d502d1998e890d8144012225de9e0450aeabd",
|
||||||
"sha256:5338a313f998e1be7267191b7caaae82563b4a2b42b393561055412a34042caa",
|
"sha256:5743f532e408cfd716fa16776b5a6447b83ff2cf39021fb5f8d052aa0f331508",
|
||||||
"sha256:5591daf81313096909d973fb2612fccd87528fdfdd39f6478bdce54543178954",
|
"sha256:5b67b869f860eb19055e2560844d8c7d0935245938935bdb764b3e683e2014e2",
|
||||||
"sha256:56896ceeffe25946c4010320629e2d858ca763cd8ded273c81672a5edbcb1e0a",
|
"sha256:5e30d8e399f38ae1ec86aa9be76d20ba15872dd0c41b4b46d1b78905857363b9",
|
||||||
"sha256:58a01ea34057463f7a98a4d6ff40160f65f945e924fec08a5b39e327e372875d",
|
"sha256:660e11b9932f58f10151d0febd11f77d3b0d48d6fa4dd4686d8983f40187101e",
|
||||||
"sha256:5bfac7f236ba1a4d402644bdce47fb9db02a7cf3214a1f637d3a88390f9e5428",
|
"sha256:67125404d12547443d74113862a80c10310cf875aff8dbfc5548fee1d9737123",
|
||||||
"sha256:5c5a8a83df95e51f7a7c2b083eaea134cbed39fc42c6aeb2e764ddb7ccccd43e",
|
"sha256:6956ef0259a037f10da767741aca82925f6f9978bb6dceb5344e56ce0629ab07",
|
||||||
"sha256:5f2fb10518f6b365f5b720e43a529c3b2324ca02932f609631a44edb347d8d54",
|
"sha256:698c37fca3761af69a09a1d39cc0492f7e8cb9e263af39a288dce8f3b8a9e2bc",
|
||||||
"sha256:64af877116edf051375b45f0bda648143176a017b13803ec7b3a3111dc05f4c5",
|
"sha256:69c8298e8197b113f765a2ea60f49ceb8e1ea9eb308140b3cdc611e0d1de70b8",
|
||||||
"sha256:6d70fa9c6d2e955bde8cd327ffc11f2cc34bc21944e5571a46ca501e7eadef24",
|
"sha256:6ef49462a615de062dcac8281944d0b036fe1e9c96a6c690bf6cf5e4b5488f0e",
|
||||||
"sha256:6d79f659965290af60d6acc8e2716341865fe74609a7ede2a33c2f86ad893b8f",
|
"sha256:6f92d12a46b2a67d56194bb5d226dabf586b386d1f1a5e25be5b745a3080dbba",
|
||||||
"sha256:720963fee259291a88348ae9735d9deb5d334e84a016244f61c89f5a49aa400a",
|
"sha256:70f70ea138e69ec3159e4a38fef52443cb8eb81388aeb241b273265ea16387c5",
|
||||||
"sha256:75a3b6e9c695a6570ea488db75b84bb592ff70a944957efa1c655867c575018b",
|
"sha256:72a0e87b2196120523fc6194ca6b580fcffa12d7daa4d57a16d7838e60f82d0e",
|
||||||
"sha256:792462abfeeca6cc8c6c1e6d27e14319682f0182f6b0ba37befe911af794db70",
|
"sha256:72b32d8c471930c95d49640ec99f7694f9b040ca1342ff03ed69d3aea90f9339",
|
||||||
"sha256:79ec4498a545733ecace48d780d22407411b07403a2e08b9a4d7596c0b97ebd7",
|
"sha256:746e25f17ba4203b5933773782cf2d30bca5cdb66a9ba5d48a53a6c795aedc57",
|
||||||
"sha256:7f14d6b303e55bd7d19b28309ef8014370e84a3806c5e452e078e7df7344d97a",
|
"sha256:75e33c9187da271d1dbeb2582ab2df2e441346492098f67559b09173ea4edde4",
|
||||||
"sha256:7f65bd5d4cdb396295b6aa07f84ca659ac65c5c68b53956a6d95219e304b0ada",
|
"sha256:7875abfd90f2ae3aa22d50f3fa1c93bbae456458cc73d3179b838f07bed1fc10",
|
||||||
"sha256:81c6b4a6c1af800d52a6fa32389ef8f4281583f4f99dc1a40f2bb47667281541",
|
"sha256:78ab0d4166cf35c73054d1e04f224af1edc3cb4d75da8b6f74f4cff7c300f358",
|
||||||
"sha256:82a07de83dce09b4fa1bccbdc8bde8f188b131666af0dc9048ba0a0e448d8a3b",
|
"sha256:78cbda57a2808d994517b53571eaa2d9299359f63aa71cf4bc94210169aad8b1",
|
||||||
"sha256:847c7f61964225fc489fe1d49eda7e0e0d253e98862c012cecf845f9ad45cdf4",
|
"sha256:7c133052737c7c75bfa49f5ba71918166fe988995b26a0d2f263a79bf8fed58a",
|
||||||
"sha256:84b52b2cf77bb7f703d16c4007b021078dbbe6cf8ffb57abe81a7bacfc175ef2",
|
"sha256:7eae9e763fbd32f19f2a66dfc2e37906f8422e0c4ad4a6c9dcf9d3246740812e",
|
||||||
"sha256:86685fa04067f7175f9718489ae755f6acde03593a1a9ca89305554af40e14fd",
|
"sha256:814bb54db2a016026efc055a3527461e5eb551c0d91b32eeade003829ff84311",
|
||||||
"sha256:8a9c962c64a4c08def58b9799333e33af94ec53038cf151d36edacdb41f81646",
|
"sha256:81ede1f094f26eeaff62e029ff1bc4e84e9d568f20d4669f64dcf7c7b18a28fc",
|
||||||
"sha256:8cbd4bfc933b33b85c43ef4c1f4d5e3e9d91975ea6368acf5fbac02bac06ea89",
|
"sha256:838fd67b3d00c5a64181073282a2c0bf8f76465da4844d5e79d2dbbc64c987dc",
|
||||||
"sha256:8ffe660e963ae711cb9e2b8d8461c9b1ad6167823837fc17d59d5e539fb898fa",
|
"sha256:89c7c125a0913d71ba9cc1fa8e14c7cfe1517b1c1f45416e1f9babcedd3b545d",
|
||||||
"sha256:94652da1a4ca7cfb69c15dd6986b205e0bd9c63a05029c3b48b4201085f527bd",
|
"sha256:8a345b5dc8ab8cafdf338e08530d48fe3f73df27f4ff569be793c7a7e7bb6b6b",
|
||||||
"sha256:969fd67de8c42affdb47b38b80f1eaa79ac0ef17d65407cdd931db1675315af1",
|
"sha256:8c3fe25871f1758519a3ad8dcafb1bd95c5d1aaeb122e6492ac739ab11fa5907",
|
||||||
"sha256:9858dcbc23ba7eaadcd319786b982278a1a2b2020720b19db43e309579ff76fb",
|
"sha256:8e12d50d4d65c74bd877e15c276992263b878456a7cfcf72521e7205a553557f",
|
||||||
"sha256:99d25aff3745c54e61ab340b98400c52ebec04290a62078155e0d7eb30380220",
|
"sha256:9adacf6fdadeeb96e6c902aef08d02d2f45429fdbf0a75b80307e435156066f8",
|
||||||
"sha256:99f55e14e7c56f4fafe1343480c32b110ef03836c21ff7c48bae7add6818f77c",
|
"sha256:9befda0dbd27ed95fba1c26be4bf98a49ba166b3c91beb5fc04364c130ce950c",
|
||||||
"sha256:9d35d8f8a1c9ed4e2b08187b513f8a3c71958600129db3aa26d85ea3abfd1310",
|
"sha256:9fa5620f7f352f9706924c0e2071a212be36421f09ee064b0fd7e1128289fcdb",
|
||||||
"sha256:a2ba92f59e30ce915e9e79af37649432e3a212ddddf416d4d686b1b4825bcdb2",
|
"sha256:a681bbf9f94027d66e15974cd207cec1a2993837b9c45acf5f6b22a67632b1c2",
|
||||||
"sha256:a2cb35a6efaa0e3623d8272471371a12c7e07b51a33e5efce9b58f655d864b4e",
|
"sha256:ab363a5baae965fb3438f2137583853ad9c77d7e45f2a62ba63e609a34d792ea",
|
||||||
"sha256:a358277122456666a8b2a0b9aa04f1b89d34e8aa41d08a6557d693e6abb6667c",
|
"sha256:ac49519fcfeb4a7ecdf6b7d0ec6cac409e59f94c1bb54630db577a97893b6e38",
|
||||||
"sha256:a60da2f9efbed355edb35a1fb6829148676786c829fad708bb6bb47211b3593a",
|
"sha256:ad5a71c1ef4a2e404c2c888db09226c821d9d14eff8813e1da873572f5fbb89d",
|
||||||
"sha256:aa7dec53c25f1949513457ef8b0ea1fb40e76c672cc4d2daa8ad3c8d6b73491a",
|
"sha256:af01133e78e5528ee282ceb1cf4bc54aecb937c2001913e751452ad7dffbbeb1",
|
||||||
"sha256:b1572ee0eb1967e71787af636bb7d1eb9c6735d5337762c450650e7f51844594",
|
"sha256:b3118ec012a799456f7fca6cc002c078590578b7640fbaab52d8ecb9a651f1c1",
|
||||||
"sha256:b4f10d3f304be7183fac79bf2cd997f82e16aa9a9f37343d76c026c6e435a8a8",
|
"sha256:b46b7aec9272fd81c984e723e599957629a91204120b3e7f0933f138e0792fdf",
|
||||||
"sha256:bbbbfbb7d039b20d289ce56b1beb46138d65769d04af50c199c6ac4cb6054d52",
|
"sha256:ba0339d6aa80615a17f47fabc5700485e9469121d658458f95cdd2003288c28b",
|
||||||
"sha256:c394a3f055b49f92626f83c1a0b6d6cd2c628f1ccd72481c3e3c6aa4695f3b20",
|
"sha256:c08cd5b19c570e1e9e094a6ce70d35bb39d12360e0763474ed9374229f174fcc",
|
||||||
"sha256:c396358023b876cff547ce87f4e8ff8a2280598873a137e8cc69e115262260b8",
|
"sha256:c0b367a00e8c88a714b2384c97dedcc85340547b3a54b6037a42fca5554d0576",
|
||||||
"sha256:c5ba07e58fd98c9782201e79eb8dd4225733d212a5a3700f9a84d329bd0463a6",
|
"sha256:c263ed9922942353a954cfbcd5f81b7626c0e20dc7f3e53d4926e8bc560ab845",
|
||||||
"sha256:c764fb167411d5afaef88138542a4bfa28bd5e5ded5e8e42df87cef965efd6e9",
|
"sha256:c3387c314695f9790dce12fcf44357197ebf773651b6a4195f5e091cf500ae73",
|
||||||
"sha256:cbba763d99de0255a3e4938f25a8579930ac8aa089233cb2fb2ed7d04d4aff02",
|
"sha256:c4fd3399eaf6f1c77ea3132611efbc3d7a8c0eb899793387b3266be221dc75fd",
|
||||||
"sha256:cbd1660fb48827381ce3a621a4fdc237959e1cd4e98b098952a8f624a0726425",
|
"sha256:cac0145491619287ff893853bf3ca4d98d5ef94b617271184a5af68a06ac301a",
|
||||||
"sha256:cd761d158f67e288fd0ebe00c3b158095ce80a4bc7c32b60c7121224003ba70d",
|
"sha256:cd9b848c17ef034f2ae31b2a1bd9276710c2baf03509f1f3fa4dc4382b0a1b53",
|
||||||
"sha256:cdfbb27f2c46bb5bbdcee147530cbc5ca8ab858d7693924e88e30ada21b2c5e2",
|
"sha256:ced27af6aee28782260bfa5643797937e96a6489bca972202834017208cf74f5",
|
||||||
"sha256:d2dbe8a3baf2d842e342e8acb06ae3844765d38df67687c144cdeb71f1bcb5d7",
|
"sha256:cf99e36c0f6ae5643ecef7ad7e1194aeb4a9798d9cff60b20ac041533fa6db0a",
|
||||||
"sha256:d5c931ec1c03111718cabf85f6012bb2815fa0ce578175567fa8d6f2cc15d3b4",
|
"sha256:d7d5f7f6f80388ce94849775da5f4082ab5e123e259972961970e190d60f5d2b",
|
||||||
"sha256:df6d7bf3e15b7e6e202a16ff4948a51759354016026deb04ab9b5acbbe35e096",
|
"sha256:dc2cab92c42991b711132bc52405680e075d1505d4356c4468bc6e9c93d49137",
|
||||||
"sha256:dfbe2aa45b273f49e715c5345d94874cb65a28482bf231af408891c260601b8d",
|
"sha256:ddcd25a1f72b2b545fb27e17882046a6c161f3f24514b2e028c00c58ed73a2dd",
|
||||||
"sha256:e12769d3adcc419e65860de946df8d2ed274932177ac1cdb05186e498aaa9149",
|
"sha256:defa3c12cb06b2fd2066a9e21bf00aab96465be84d9585c8c05195f080510506",
|
||||||
"sha256:e5aed4754895942ae15ffa48c52d181e1c1463236fda68d2dba29c03aa61786b",
|
"sha256:df3ffa6ef88166bb36f5d1492e71e664868b9b8b6afd55821e0ac0cb96625441",
|
||||||
"sha256:e645b4c365d6f1f9e0799380ad6395268f3c3b898244a650aaeb8d9d27b74c35",
|
"sha256:e127b2e7fc533728295196f9265d12834530f475bc6cd6f74619df415d04b8b1",
|
||||||
"sha256:ed3493a369fe849cce98542d7405c88030b355e4d2e113887cb7ecc86c205773",
|
"sha256:e9ff57452fcadfd1a379314234657b8f4e9967ae64480ddf7c2fd82139bc8cf8",
|
||||||
"sha256:f08342dc8d19214faa7ef99574dea6c37a2790d6d04a9793ef8fa76c188dc08d",
|
"sha256:eb9fa02b9f5bbdb1d036a0c68999337793fa244528e0ce825e4b97cb7f7db99f",
|
||||||
"sha256:f0a55cf02d2cdd739b40ce10c09bbdd520e141217696add7a48b56e67bdfdfe6",
|
"sha256:ec3d0a6c37a816ea6e3550697c60d90861c9b0f982a98a40b59ac1f7a360bfa9",
|
||||||
"sha256:f0ce9d9e07c75da8027c62b4c9f45771d1d8aae7dc9ad7fb606c6a5aedbe9741",
|
"sha256:ef123b6aead12e0784f72970e8d94a96ac0d0aa4438c7ab9235e2f8669a0a5ae",
|
||||||
"sha256:f1f723e260c35e1c7c57a09d3a6ebe681bd56c83e1208ae3ce1869b7c0d10d4f",
|
"sha256:f90bb3c680804ec9630bcf8c0965e54b4de84d33b17d7da57c87c30f0c64c6f5",
|
||||||
"sha256:f2fcbe9b3a5996b417e030aa33a562e7e20dfc86271e53d7e841fc5df16268b8",
|
"sha256:fb1df55f5700187c6db4b47dbdaf8a0653a111341ac7fccc596b397aa3399e65",
|
||||||
"sha256:f3fd8df63c41ff6a676d031956aebf68ebbc687b47c507da25501eb22eec341f",
|
"sha256:fd68db1a0f5d9374a7b735414efe48d2b3ecbf0adea39299bb48030988f16149",
|
||||||
"sha256:f8d3417db5e14a6789073b21ae44439a755289477901901bae378a57b905e148",
|
"sha256:fd6b5b6df14f98b2e7e474c1c7ea55fc32dcab038b3b34b76a591dec8ba50915",
|
||||||
"sha256:fbf05678c2ae0064fb1b8de7e9e8f0fc30621b73c8477786dd0fb3868044a8c8",
|
"sha256:fd7e3158be382f8f140caccc0dc7742a7553ce4bf2978982abe3054d2cedd705",
|
||||||
"sha256:fc98ff43abad61f00515ad9a06213b7716699146e46eabd256cdfe7cb522bd97",
|
"sha256:fe989bbed9d6f332c1e24d333936f3fa1375f380cd8028da0b985dcdefa6015a",
|
||||||
"sha256:ff1863f037dad765ef5963efc2e37d399ac023e192a6f2bb394e2377d023cefe"
|
"sha256:ff2ca039a161d49a8c713f5380def315c6f793df5fe348b94782b1dbee37a644"
|
||||||
],
|
],
|
||||||
"version": "==3.8.0"
|
"version": "==3.8.1"
|
||||||
},
|
},
|
||||||
"bitstring": {
|
"bitstring": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -257,207 +257,208 @@
|
|||||||
},
|
},
|
||||||
"charset-normalizer": {
|
"charset-normalizer": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e",
|
"sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc",
|
||||||
"sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c",
|
"sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c",
|
||||||
"sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5",
|
"sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67",
|
||||||
"sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815",
|
"sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4",
|
||||||
"sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f",
|
"sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0",
|
||||||
"sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0",
|
"sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c",
|
||||||
"sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484",
|
"sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5",
|
||||||
"sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407",
|
"sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444",
|
||||||
"sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6",
|
"sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153",
|
||||||
"sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8",
|
"sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9",
|
||||||
"sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264",
|
"sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01",
|
||||||
"sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815",
|
"sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217",
|
||||||
"sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2",
|
"sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b",
|
||||||
"sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4",
|
"sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c",
|
||||||
"sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579",
|
"sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a",
|
||||||
"sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f",
|
"sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83",
|
||||||
"sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa",
|
"sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5",
|
||||||
"sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95",
|
"sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7",
|
||||||
"sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab",
|
"sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb",
|
||||||
"sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297",
|
"sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c",
|
||||||
"sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a",
|
"sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1",
|
||||||
"sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e",
|
"sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42",
|
||||||
"sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84",
|
"sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab",
|
||||||
"sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8",
|
"sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df",
|
||||||
"sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0",
|
"sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e",
|
||||||
"sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9",
|
"sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207",
|
||||||
"sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f",
|
"sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18",
|
||||||
"sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1",
|
"sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734",
|
||||||
"sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843",
|
"sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38",
|
||||||
"sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565",
|
"sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110",
|
||||||
"sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7",
|
"sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18",
|
||||||
"sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c",
|
"sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44",
|
||||||
"sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b",
|
"sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d",
|
||||||
"sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7",
|
"sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48",
|
||||||
"sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687",
|
"sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e",
|
||||||
"sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9",
|
"sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5",
|
||||||
"sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14",
|
"sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d",
|
||||||
"sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89",
|
"sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53",
|
||||||
"sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f",
|
"sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790",
|
||||||
"sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0",
|
"sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c",
|
||||||
"sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9",
|
"sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b",
|
||||||
"sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a",
|
"sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116",
|
||||||
"sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389",
|
"sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d",
|
||||||
"sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0",
|
"sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10",
|
||||||
"sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30",
|
"sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6",
|
||||||
"sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd",
|
"sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2",
|
||||||
"sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e",
|
"sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776",
|
||||||
"sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9",
|
"sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a",
|
||||||
"sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc",
|
"sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265",
|
||||||
"sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532",
|
"sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008",
|
||||||
"sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d",
|
"sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943",
|
||||||
"sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae",
|
"sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374",
|
||||||
"sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2",
|
"sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246",
|
||||||
"sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64",
|
"sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e",
|
||||||
"sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f",
|
"sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5",
|
||||||
"sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557",
|
"sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616",
|
||||||
"sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e",
|
"sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15",
|
||||||
"sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff",
|
"sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41",
|
||||||
"sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398",
|
"sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960",
|
||||||
"sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db",
|
"sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752",
|
||||||
"sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a",
|
"sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e",
|
||||||
"sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43",
|
"sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72",
|
||||||
"sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597",
|
"sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7",
|
||||||
"sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c",
|
"sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8",
|
||||||
"sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e",
|
"sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b",
|
||||||
"sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2",
|
"sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4",
|
||||||
"sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54",
|
"sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545",
|
||||||
"sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e",
|
"sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706",
|
||||||
"sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4",
|
"sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366",
|
||||||
"sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4",
|
"sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb",
|
||||||
"sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7",
|
"sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a",
|
||||||
"sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6",
|
"sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e",
|
||||||
"sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5",
|
"sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00",
|
||||||
"sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194",
|
"sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f",
|
||||||
"sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69",
|
"sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a",
|
||||||
"sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f",
|
"sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1",
|
||||||
"sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316",
|
"sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66",
|
||||||
"sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e",
|
"sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356",
|
||||||
"sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73",
|
"sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319",
|
||||||
"sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8",
|
"sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4",
|
||||||
"sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923",
|
"sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad",
|
||||||
"sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88",
|
"sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d",
|
||||||
"sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f",
|
"sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5",
|
||||||
"sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21",
|
"sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7",
|
||||||
"sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4",
|
"sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0",
|
||||||
"sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6",
|
"sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686",
|
||||||
"sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc",
|
"sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34",
|
||||||
"sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2",
|
"sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49",
|
||||||
"sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866",
|
"sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c",
|
||||||
"sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021",
|
"sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1",
|
||||||
"sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2",
|
"sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e",
|
||||||
"sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d",
|
"sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60",
|
||||||
"sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8",
|
"sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0",
|
||||||
"sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de",
|
"sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274",
|
||||||
"sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237",
|
"sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d",
|
||||||
"sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4",
|
"sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0",
|
||||||
"sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778",
|
"sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae",
|
||||||
"sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb",
|
"sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f",
|
||||||
"sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc",
|
"sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d",
|
||||||
"sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602",
|
"sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe",
|
||||||
"sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4",
|
"sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3",
|
||||||
"sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f",
|
"sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393",
|
||||||
"sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5",
|
"sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1",
|
||||||
"sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611",
|
"sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af",
|
||||||
"sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8",
|
"sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44",
|
||||||
"sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf",
|
"sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00",
|
||||||
"sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d",
|
"sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c",
|
||||||
"sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b",
|
"sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3",
|
||||||
"sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db",
|
"sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7",
|
||||||
"sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e",
|
"sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd",
|
||||||
"sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077",
|
"sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e",
|
||||||
"sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd",
|
"sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b",
|
||||||
"sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef",
|
"sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8",
|
||||||
"sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e",
|
"sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259",
|
||||||
"sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8",
|
"sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859",
|
||||||
"sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe",
|
"sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46",
|
||||||
"sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058",
|
"sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30",
|
||||||
"sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17",
|
"sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b",
|
||||||
"sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833",
|
"sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46",
|
||||||
"sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421",
|
"sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24",
|
||||||
"sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550",
|
"sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a",
|
||||||
"sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff",
|
"sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24",
|
||||||
"sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2",
|
"sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc",
|
||||||
"sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc",
|
"sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215",
|
||||||
"sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982",
|
"sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063",
|
||||||
"sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d",
|
"sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832",
|
||||||
"sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed",
|
"sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6",
|
||||||
"sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104",
|
"sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79",
|
||||||
"sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"
|
"sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==3.4.6"
|
"version": "==3.4.7"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a",
|
"sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5",
|
||||||
"sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"
|
"sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.10'",
|
"markers": "python_version >= '3.10'",
|
||||||
"version": "==8.3.1"
|
"version": "==8.3.2"
|
||||||
},
|
},
|
||||||
"cryptography": {
|
"cryptography": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72",
|
"sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65",
|
||||||
"sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235",
|
"sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832",
|
||||||
"sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9",
|
"sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067",
|
||||||
"sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356",
|
"sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de",
|
||||||
"sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257",
|
"sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4",
|
||||||
"sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad",
|
"sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0",
|
||||||
"sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4",
|
"sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b",
|
||||||
"sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c",
|
"sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968",
|
||||||
"sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614",
|
"sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef",
|
||||||
"sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed",
|
"sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b",
|
||||||
"sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31",
|
"sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4",
|
||||||
"sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229",
|
"sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3",
|
||||||
"sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0",
|
"sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308",
|
||||||
"sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731",
|
"sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e",
|
||||||
"sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b",
|
"sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163",
|
||||||
"sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4",
|
"sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f",
|
||||||
"sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4",
|
"sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee",
|
||||||
"sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263",
|
"sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77",
|
||||||
"sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595",
|
"sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85",
|
||||||
"sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1",
|
"sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99",
|
||||||
"sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678",
|
"sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7",
|
||||||
"sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48",
|
"sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83",
|
||||||
"sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76",
|
"sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85",
|
||||||
"sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0",
|
"sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006",
|
||||||
"sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18",
|
"sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb",
|
||||||
"sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d",
|
"sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e",
|
||||||
"sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d",
|
"sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba",
|
||||||
"sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1",
|
"sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325",
|
||||||
"sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981",
|
"sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d",
|
||||||
"sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7",
|
"sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1",
|
||||||
"sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82",
|
"sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1",
|
||||||
"sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2",
|
"sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2",
|
||||||
"sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4",
|
"sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0",
|
||||||
"sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663",
|
"sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455",
|
||||||
"sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c",
|
"sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842",
|
||||||
"sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d",
|
"sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457",
|
||||||
"sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a",
|
"sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15",
|
||||||
"sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a",
|
"sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2",
|
||||||
"sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d",
|
"sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c",
|
||||||
"sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b",
|
"sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb",
|
||||||
"sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a",
|
"sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5",
|
||||||
"sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826",
|
"sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4",
|
||||||
"sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee",
|
"sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902",
|
||||||
"sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9",
|
"sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246",
|
||||||
"sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648",
|
"sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022",
|
||||||
"sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da",
|
"sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f",
|
||||||
"sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2",
|
"sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e",
|
||||||
"sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2",
|
"sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298",
|
||||||
"sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"
|
"sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'",
|
"markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'",
|
||||||
"version": "==46.0.5"
|
"version": "==46.0.7"
|
||||||
},
|
},
|
||||||
"esptool": {
|
"esptool": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9c355b7d6331cc92979cc710ae5c41f59830d1ea29ec24c467c6005a092c06d6"
|
"sha256:9c355b7d6331cc92979cc710ae5c41f59830d1ea29ec24c467c6005a092c06d6"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
"version": "==5.2.0"
|
"version": "==5.2.0"
|
||||||
},
|
},
|
||||||
"h11": {
|
"h11": {
|
||||||
@@ -505,15 +506,17 @@
|
|||||||
"sha256:abfb82ca31cc430174e4761cc7356adc4bff00ea758d437c2b258883dc63f464"
|
"sha256:abfb82ca31cc430174e4761cc7356adc4bff00ea758d437c2b258883dc63f464"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.6.0"
|
"version": "==2.6.0"
|
||||||
},
|
},
|
||||||
"mpremote": {
|
"mpremote": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:11d134c69b21b487dae3d03eed54c8ccbf84c916c8732a3e069a97cae47be3d4",
|
"sha256:2df2a50f3c8098cae8c732dbf2541e7e58185e7896513b45d05196901e049334",
|
||||||
"sha256:6bb75774648091dad6833af4f86c5bf6505f8d7aec211380f9e6996c01d23cb5"
|
"sha256:fdb5626be83dff4e53c0184f8950814cb519b524dba7f1f8b1668aa477257a31"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.27.0"
|
"markers": "python_version >= '3.4'",
|
||||||
|
"version": "==1.28.0"
|
||||||
},
|
},
|
||||||
"outcome": {
|
"outcome": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -525,11 +528,11 @@
|
|||||||
},
|
},
|
||||||
"platformdirs": {
|
"platformdirs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934",
|
"sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a",
|
||||||
"sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"
|
"sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.10'",
|
"markers": "python_version >= '3.10'",
|
||||||
"version": "==4.9.4"
|
"version": "==4.9.6"
|
||||||
},
|
},
|
||||||
"pycparser": {
|
"pycparser": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -541,11 +544,11 @@
|
|||||||
},
|
},
|
||||||
"pygments": {
|
"pygments": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887",
|
"sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f",
|
||||||
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"
|
"sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==2.19.2"
|
"version": "==2.20.0"
|
||||||
},
|
},
|
||||||
"pyjwt": {
|
"pyjwt": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -553,6 +556,7 @@
|
|||||||
"sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"
|
"sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==2.12.1"
|
"version": "==2.12.1"
|
||||||
},
|
},
|
||||||
"pyserial": {
|
"pyserial": {
|
||||||
@@ -667,19 +671,20 @@
|
|||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6",
|
"sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517",
|
||||||
"sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"
|
"sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.32.5"
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==2.33.1"
|
||||||
},
|
},
|
||||||
"rich": {
|
"rich": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d",
|
"sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb",
|
||||||
"sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"
|
"sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.8.0'",
|
"markers": "python_full_version >= '3.9.0'",
|
||||||
"version": "==14.3.3"
|
"version": "==15.0.0"
|
||||||
},
|
},
|
||||||
"rich-click": {
|
"rich-click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -691,11 +696,12 @@
|
|||||||
},
|
},
|
||||||
"selenium": {
|
"selenium": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:003e971f805231ad63e671783a2b91a299355d10cefb9de964c36ff3819115aa",
|
"sha256:4f97639055dcfa9eadf8ccf549ba7b0e49c655d4e2bde19b9a44e916b754e769",
|
||||||
"sha256:b8ccde8d2e7642221ca64af184a92c19eee6accf2e27f20f30472f5efae18eb1"
|
"sha256:bada5c08a989f812728a4b5bea884d8e91894e939a441cc3a025201ce718581e"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.41.0"
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==4.43.0"
|
||||||
},
|
},
|
||||||
"sniffio": {
|
"sniffio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -774,6 +780,9 @@
|
|||||||
"version": "==4.15.0"
|
"version": "==4.15.0"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
|
"extras": [
|
||||||
|
"socks"
|
||||||
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed",
|
"sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed",
|
||||||
"sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"
|
"sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"
|
||||||
@@ -894,6 +903,7 @@
|
|||||||
"sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf"
|
"sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==1.1.1"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
"websocket-client": {
|
"websocket-client": {
|
||||||
@@ -904,6 +914,74 @@
|
|||||||
"markers": "python_version >= '3.9'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==1.9.0"
|
"version": "==1.9.0"
|
||||||
},
|
},
|
||||||
|
"websockets": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c",
|
||||||
|
"sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a",
|
||||||
|
"sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe",
|
||||||
|
"sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e",
|
||||||
|
"sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec",
|
||||||
|
"sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1",
|
||||||
|
"sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64",
|
||||||
|
"sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3",
|
||||||
|
"sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8",
|
||||||
|
"sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206",
|
||||||
|
"sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3",
|
||||||
|
"sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156",
|
||||||
|
"sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d",
|
||||||
|
"sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9",
|
||||||
|
"sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad",
|
||||||
|
"sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2",
|
||||||
|
"sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03",
|
||||||
|
"sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8",
|
||||||
|
"sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230",
|
||||||
|
"sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8",
|
||||||
|
"sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea",
|
||||||
|
"sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641",
|
||||||
|
"sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957",
|
||||||
|
"sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6",
|
||||||
|
"sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6",
|
||||||
|
"sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5",
|
||||||
|
"sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f",
|
||||||
|
"sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00",
|
||||||
|
"sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e",
|
||||||
|
"sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b",
|
||||||
|
"sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72",
|
||||||
|
"sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39",
|
||||||
|
"sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9",
|
||||||
|
"sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79",
|
||||||
|
"sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0",
|
||||||
|
"sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac",
|
||||||
|
"sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35",
|
||||||
|
"sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0",
|
||||||
|
"sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5",
|
||||||
|
"sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c",
|
||||||
|
"sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8",
|
||||||
|
"sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1",
|
||||||
|
"sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244",
|
||||||
|
"sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3",
|
||||||
|
"sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767",
|
||||||
|
"sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a",
|
||||||
|
"sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d",
|
||||||
|
"sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd",
|
||||||
|
"sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e",
|
||||||
|
"sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944",
|
||||||
|
"sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82",
|
||||||
|
"sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d",
|
||||||
|
"sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4",
|
||||||
|
"sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5",
|
||||||
|
"sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904",
|
||||||
|
"sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde",
|
||||||
|
"sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f",
|
||||||
|
"sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c",
|
||||||
|
"sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89",
|
||||||
|
"sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da",
|
||||||
|
"sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==16.0"
|
||||||
|
},
|
||||||
"wsproto": {
|
"wsproto": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584",
|
"sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584",
|
||||||
@@ -940,19 +1018,20 @@
|
|||||||
},
|
},
|
||||||
"pygments": {
|
"pygments": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887",
|
"sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f",
|
||||||
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"
|
"sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==2.19.2"
|
"version": "==2.20.0"
|
||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b",
|
"sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9",
|
||||||
"sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"
|
"sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==9.0.2"
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==9.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"f0f5bdfb9d30": {"id": "f0f5bdfb9d30", "name": "a", "type": "led", "transport": "wifi", "address": "10.1.1.182", "default_pattern": null, "zones": []}, "188b0e1560a8": {"id": "188b0e1560a8", "name": "a", "type": "led", "transport": "wifi", "address": "10.1.1.242", "default_pattern": null, "zones": []}, "24ec4acaffcc": {"id": "24ec4acaffcc", "name": "c", "type": "led", "transport": "wifi", "address": "10.1.1.171", "default_pattern": null, "zones": []}}
|
{"24ec4acaffcc": {"id": "24ec4acaffcc", "name": "a", "type": "led", "transport": "wifi", "address": "10.1.1.171", "default_pattern": null, "zones": []}, "188b0e1560a8": {"id": "188b0e1560a8", "name": "led-188b0e1560a8", "type": "led", "transport": "wifi", "address": "10.1.1.192", "default_pattern": null, "zones": []}, "f0f5bdfb9d30": {"id": "f0f5bdfb9d30", "name": "led-f0f5bdfb9d30", "type": "led", "transport": "wifi", "address": "10.1.1.232", "default_pattern": null, "zones": []}}
|
||||||
@@ -1 +1 @@
|
|||||||
{"1": {"name": "default", "names": ["a", "c", "a"], "presets": [["4", "2", "7"], ["3", "14", "5"], ["8", "10", "11"], ["9", "12", "1"], ["13", "37", "6"]], "presets_flat": ["4", "2", "7", "3", "14", "5", "8", "10", "11", "9", "12", "1", "13", "37", "6"], "default_preset": "15"}, "2": {"name": "default", "names": ["1", "2", "3", "4", "5", "6", "7", "8", "0", "a"], "presets": [["16", "17", "18"], ["19", "20", "21"], ["22", "23", "24"], ["25", "26", "27"], ["28", "29", "30"]], "presets_flat": ["16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]}, "3": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "4": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "5": {"name": "dj", "names": ["dj"], "presets": [["31", "32", "33"]], "default_preset": "31", "presets_flat": ["31", "32", "33"]}, "6": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "7": {"name": "dj", "names": ["dj"], "presets": [["34", "35", "36"]], "default_preset": "34", "presets_flat": ["34", "35", "36"]}, "8": {"name": "test", "names": ["11"], "presets": [["1", "2", "3"], ["4", "5"]], "default_preset": "1", "presets_flat": ["1", "2", "3", "4", "5"]}}
|
{"1": {"name": "default", "names": ["led-188b0e1560a8", "a", "led-f0f5bdfb9d30"], "presets": [["4", "2", "7"], ["3", "14", "5"], ["8", "10", "11"], ["9", "12", "1"], ["13", "37", "6"]], "presets_flat": ["4", "2", "7", "3", "14", "5", "8", "10", "11", "9", "12", "1", "13", "37", "6"], "default_preset": "15"}, "2": {"name": "default", "names": ["1", "2", "3", "4", "5", "6", "7", "8", "0", "a"], "presets": [["16", "17", "18"], ["19", "20", "21"], ["22", "23", "24"], ["25", "26", "27"], ["28", "29", "30"]], "presets_flat": ["16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]}, "3": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "4": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "5": {"name": "dj", "names": ["dj"], "presets": [["31", "32", "33"]], "default_preset": "31", "presets_flat": ["31", "32", "33"]}, "6": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "7": {"name": "dj", "names": ["dj"], "presets": [["34", "35", "36"]], "default_preset": "34", "presets_flat": ["34", "35", "36"]}, "8": {"name": "test", "names": ["11"], "presets": [["1", "2", "3"], ["4", "5"]], "default_preset": "1", "presets_flat": ["1", "2", "3", "4", "5"]}}
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
This document covers:
|
This document covers:
|
||||||
|
|
||||||
1. **HTTP and WebSocket** exposed by the Raspberry Pi app (`src/main.py`) — profiles, zones, presets, transport send, pattern OTA helpers, and related resources.
|
1. **HTTP and WebSocket** exposed by the Raspberry Pi app (`src/main.py`) — profiles, zones, presets, transport send, pattern OTA helpers, and related resources.
|
||||||
2. **LED driver JSON** — the compact **v1** message format. It is sent over the **serial → ESP-NOW bridge** to ESP32 peers and as **newline-delimited JSON over TCP** to **Wi-Fi** drivers (same logical fields).
|
2. **LED driver JSON** — the compact **v1** message format. It is sent over the **serial → ESP-NOW bridge** to ESP32 peers and as **single JSON text messages** over the **outbound WebSocket** to **Wi-Fi** drivers (same logical fields).
|
||||||
|
|
||||||
Default HTTP listen address: `0.0.0.0`. Port defaults to **80**; override with the **`PORT`** environment variable (see `pipenv run run`).
|
Default HTTP listen address: `0.0.0.0`. Port defaults to **80**; override with the **`PORT`** environment variable (see `pipenv run run`).
|
||||||
|
|
||||||
**Serial:** UART path and baud come from settings (defaults include `serial_port` such as `/dev/ttyS0` and `serial_baudrate`). **Wi-Fi drivers:** the Pi accepts TCP connections on **`tcp_port`** in settings (default **8765**). **UDP discovery** listens on **8766** so drivers can find the controller IP on the LAN.
|
**Serial:** UART path and baud come from settings (defaults include `serial_port` such as `/dev/ttyS0` and `serial_baudrate`). **Wi-Fi drivers:** **UDP** on port **8766** is the **discovery** channel: each driver’s JSON hello (**`device_name`**, **MAC**, optional **`type`**) **creates or updates** that device in **`db/device.json`** (keyed by MAC); the Pi echoes the datagram. After a valid hello with **`v`:** **`"1"`**, the Pi also opens an **outbound WebSocket** to that IP (**`wifi_driver_ws_port`**, default **80**; **`wifi_driver_ws_path`**, default **`/ws`**) for v1 commands; presets are not pushed automatically on connect (use **Send Presets** / profile apply). The Pi may send periodic UDP **hello** nudges to known Wi‑Fi device IPs when the WebSocket is down (**`wifi_driver_hello_interval_s`** in settings).
|
||||||
|
|
||||||
All JSON APIs use `Content-Type: application/json` for bodies and responses unless noted.
|
All JSON APIs use `Content-Type: application/json` for bodies and responses unless noted.
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ Pattern metadata lives in **`db/pattern.json`**; driver source files live under
|
|||||||
|
|
||||||
## LED driver message format (transport / ESP-NOW / Wi-Fi)
|
## LED driver message format (transport / ESP-NOW / Wi-Fi)
|
||||||
|
|
||||||
Messages are JSON objects. The Pi **`build_message()`** helper (`src/util/espnow_message.py`) produces the same shape sent over serial and forwarded by the ESP32 bridge, and the same logical object can be sent as a **single JSON line** to a Wi-Fi driver over TCP.
|
Messages are JSON objects. The Pi **`build_message()`** helper (`src/util/espnow_message.py`) produces the same shape sent over serial and forwarded by the ESP32 bridge, and the same logical object can be sent as a **single JSON text message** to a Wi-Fi driver over the **WebSocket**.
|
||||||
|
|
||||||
### Top-level fields
|
### Top-level fields
|
||||||
|
|
||||||
|
|||||||
Submodule led-driver updated: ded6e3d360...87bd0338bd
2
led-tool
2
led-tool
Submodule led-tool updated: eee9327e15...9e72c62481
@@ -6,7 +6,7 @@ from models.device import (
|
|||||||
validate_device_type,
|
validate_device_type,
|
||||||
)
|
)
|
||||||
from models.transport import get_current_sender
|
from models.transport import get_current_sender
|
||||||
from models.tcp_clients import (
|
from models.wifi_ws_clients import (
|
||||||
normalize_tcp_peer_ip,
|
normalize_tcp_peer_ip,
|
||||||
send_json_line_to_ip,
|
send_json_line_to_ip,
|
||||||
tcp_client_connected,
|
tcp_client_connected,
|
||||||
@@ -56,8 +56,8 @@ devices = Device()
|
|||||||
|
|
||||||
def _device_live_connected(dev_dict):
|
def _device_live_connected(dev_dict):
|
||||||
"""
|
"""
|
||||||
Wi-Fi: whether a TCP client is registered for this device's address (IP).
|
Wi-Fi: whether the controller has an outbound WebSocket to this device's IP.
|
||||||
ESP-NOW: None (no TCP session on the Pi for that transport).
|
ESP-NOW: None (no Wi-Fi session on the Pi for that transport).
|
||||||
"""
|
"""
|
||||||
tr = (dev_dict.get("transport") or "espnow").strip().lower()
|
tr = (dev_dict.get("transport") or "espnow").strip().lower()
|
||||||
if tr != "wifi":
|
if tr != "wifi":
|
||||||
@@ -114,7 +114,7 @@ async def _identify_send_off_after_delay(sender, transport, wifi_ip, dev_id, nam
|
|||||||
|
|
||||||
@controller.get("")
|
@controller.get("")
|
||||||
async def list_devices(request):
|
async def list_devices(request):
|
||||||
"""List all devices (includes ``connected`` for live Wi-Fi TCP presence)."""
|
"""List all devices (includes ``connected`` for live Wi-Fi WebSocket presence)."""
|
||||||
devices_data = {}
|
devices_data = {}
|
||||||
for dev_id in devices.list():
|
for dev_id in devices.list():
|
||||||
d = devices.read(dev_id)
|
d = devices.read(dev_id)
|
||||||
@@ -125,7 +125,7 @@ async def list_devices(request):
|
|||||||
|
|
||||||
@controller.get("/<id>")
|
@controller.get("/<id>")
|
||||||
async def get_device(request, id):
|
async def get_device(request, id):
|
||||||
"""Get a device by ID (includes ``connected`` for live Wi-Fi TCP presence)."""
|
"""Get a device by ID (includes ``connected`` for live Wi-Fi WebSocket presence)."""
|
||||||
dev = devices.read(id)
|
dev = devices.read(id)
|
||||||
if dev:
|
if dev:
|
||||||
return json.dumps(_device_json_with_live_status(dev)), 200, {
|
return json.dumps(_device_json_with_live_status(dev)), 200, {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from microdot import Microdot
|
from microdot import Microdot
|
||||||
from models.pattern import Pattern
|
from models.pattern import Pattern
|
||||||
from models.device import Device
|
from models.device import Device
|
||||||
from models.tcp_clients import send_json_line_to_ip
|
from models.wifi_ws_clients import send_json_line_to_ip
|
||||||
from util.driver_patterns import (
|
from util.driver_patterns import (
|
||||||
driver_patterns_dir,
|
driver_patterns_dir,
|
||||||
is_firmware_builtin_pattern_module,
|
is_firmware_builtin_pattern_module,
|
||||||
|
|||||||
302
src/main.py
302
src/main.py
@@ -23,7 +23,7 @@ import controllers.settings as settings_controller
|
|||||||
import controllers.device as device_controller
|
import controllers.device as device_controller
|
||||||
from models.transport import get_sender, set_sender, get_current_sender
|
from models.transport import get_sender, set_sender, get_current_sender
|
||||||
from models.device import Device, normalize_mac
|
from models.device import Device, normalize_mac
|
||||||
from models import tcp_clients as tcp_client_registry
|
from models import wifi_ws_clients as tcp_client_registry
|
||||||
from util.device_status_broadcaster import (
|
from util.device_status_broadcaster import (
|
||||||
broadcast_device_tcp_snapshot_to,
|
broadcast_device_tcp_snapshot_to,
|
||||||
broadcast_device_tcp_status,
|
broadcast_device_tcp_status,
|
||||||
@@ -33,82 +33,8 @@ from util.device_status_broadcaster import (
|
|||||||
|
|
||||||
_tcp_device_lock = threading.Lock()
|
_tcp_device_lock = threading.Lock()
|
||||||
|
|
||||||
# Wi-Fi drivers send one hello line then stay quiet; periodic outbound data makes dead peers
|
|
||||||
# fail drain() within this interval (keepalive alone is often slow or ineffective).
|
|
||||||
TCP_LIVENESS_PING_INTERVAL_S = 12.0
|
|
||||||
DISCOVERY_UDP_PORT = 8766
|
DISCOVERY_UDP_PORT = 8766
|
||||||
|
|
||||||
# Keepalive or lossy Wi-Fi can still surface OSError(110) / TimeoutError on recv or wait_closed.
|
|
||||||
_TCP_PEER_GONE = (
|
|
||||||
BrokenPipeError,
|
|
||||||
ConnectionResetError,
|
|
||||||
ConnectionAbortedError,
|
|
||||||
ConnectionRefusedError,
|
|
||||||
TimeoutError,
|
|
||||||
OSError,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _tcp_socket_from_writer(writer):
|
|
||||||
sock = writer.get_extra_info("socket")
|
|
||||||
if sock is not None:
|
|
||||||
return sock
|
|
||||||
transport = getattr(writer, "transport", None)
|
|
||||||
if transport is not None:
|
|
||||||
return transport.get_extra_info("socket")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _enable_tcp_keepalive(writer) -> None:
|
|
||||||
"""
|
|
||||||
Detect vanished peers (power off, Wi-Fi drop) without waiting for a send() failure.
|
|
||||||
Linux: shorten time before the first keepalive probe; other platforms: SO_KEEPALIVE only.
|
|
||||||
"""
|
|
||||||
sock = _tcp_socket_from_writer(writer)
|
|
||||||
if sock is None:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
|
||||||
except OSError:
|
|
||||||
return
|
|
||||||
if hasattr(socket, "TCP_KEEPIDLE"):
|
|
||||||
try:
|
|
||||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 120)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
if hasattr(socket, "TCP_KEEPINTVL"):
|
|
||||||
try:
|
|
||||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
if hasattr(socket, "TCP_KEEPCNT"):
|
|
||||||
try:
|
|
||||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
# Do not set TCP_USER_TIMEOUT: a short value causes Errno 110 on recv for Wi-Fi peers
|
|
||||||
# when ACKs are delayed (ESP power save, lossy links). Liveness pings already clear dead
|
|
||||||
# sessions via drain().
|
|
||||||
|
|
||||||
|
|
||||||
async def _tcp_liveness_ping_loop(writer, peer_ip: str) -> None:
|
|
||||||
"""Send a bare newline so ``drain()`` fails soon after the peer disappears."""
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(TCP_LIVENESS_PING_INTERVAL_S)
|
|
||||||
if writer.is_closing():
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
writer.write(b"\n")
|
|
||||||
await writer.drain()
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"[TCP] liveness ping failed {peer_ip!r}: {exc!r}")
|
|
||||||
tcp_client_registry.unregister_tcp_writer(peer_ip, writer)
|
|
||||||
try:
|
|
||||||
writer.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def _register_udp_device_sync(
|
def _register_udp_device_sync(
|
||||||
device_name: str, peer_ip: str, mac, device_type=None
|
device_name: str, peer_ip: str, mac, device_type=None
|
||||||
@@ -116,10 +42,10 @@ def _register_udp_device_sync(
|
|||||||
with _tcp_device_lock:
|
with _tcp_device_lock:
|
||||||
try:
|
try:
|
||||||
d = Device()
|
d = Device()
|
||||||
did = d.upsert_wifi_tcp_client(
|
did, persisted = d.upsert_wifi_tcp_client(
|
||||||
device_name, peer_ip, mac, device_type=device_type
|
device_name, peer_ip, mac, device_type=device_type
|
||||||
)
|
)
|
||||||
if did:
|
if did and persisted:
|
||||||
print(
|
print(
|
||||||
f"UDP device registered: mac={did} name={device_name!r} ip={peer_ip!r}"
|
f"UDP device registered: mac={did} name={device_name!r} ip={peer_ip!r}"
|
||||||
)
|
)
|
||||||
@@ -155,6 +81,8 @@ async def _handle_udp_discovery(sock, udp_holder=None) -> None:
|
|||||||
device_type = parsed.get("type") or parsed.get("device_type")
|
device_type = parsed.get("type") or parsed.get("device_type")
|
||||||
if dns and normalize_mac(mac):
|
if dns and normalize_mac(mac):
|
||||||
_register_udp_device_sync(dns, peer_ip, mac, device_type)
|
_register_udp_device_sync(dns, peer_ip, mac, device_type)
|
||||||
|
if str(parsed.get("v") or "") == "1":
|
||||||
|
tcp_client_registry.ensure_driver_connection(peer_ip)
|
||||||
except (UnicodeError, ValueError, TypeError):
|
except (UnicodeError, ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
@@ -163,6 +91,109 @@ async def _handle_udp_discovery(sock, udp_holder=None) -> None:
|
|||||||
print(f"[UDP] echo send failed: {e!r}")
|
print(f"[UDP] echo send failed: {e!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def _prime_wifi_outbound_driver_connections() -> None:
|
||||||
|
"""
|
||||||
|
For each Wi‑Fi device in the registry with a usable IPv4, start (or keep) the
|
||||||
|
outbound WebSocket task. The client loop reconnects automatically if the link
|
||||||
|
drops. Presets are not pushed automatically; use Send Presets / profile apply.
|
||||||
|
"""
|
||||||
|
n = 0
|
||||||
|
try:
|
||||||
|
dev = Device()
|
||||||
|
for mac_key, doc in list(dev.items()):
|
||||||
|
if not isinstance(doc, dict):
|
||||||
|
continue
|
||||||
|
if doc.get("transport") != "wifi":
|
||||||
|
continue
|
||||||
|
ip = _ipv4_address(str(doc.get("address") or ""))
|
||||||
|
if not ip:
|
||||||
|
continue
|
||||||
|
tcp_client_registry.ensure_driver_connection(ip)
|
||||||
|
n += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[startup] Wi-Fi driver connection prime failed: {e!r}")
|
||||||
|
traceback.print_exception(type(e), e, e.__traceback__)
|
||||||
|
return
|
||||||
|
if n:
|
||||||
|
print(f"[startup] primed outbound WebSocket for {n} Wi-Fi driver(s)")
|
||||||
|
|
||||||
|
|
||||||
|
def _ipv4_address(addr: str) -> str | None:
|
||||||
|
"""Return dotted IPv4 string or None (hostnames skipped for UDP nudge)."""
|
||||||
|
s = (addr or "").strip()
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
parts = s.split(".")
|
||||||
|
if len(parts) != 4:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
nums = [int(p) for p in parts]
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if not all(0 <= n <= 255 for n in nums):
|
||||||
|
return None
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
async def _periodic_wifi_driver_hello_loop(settings, udp_holder) -> None:
|
||||||
|
"""
|
||||||
|
While a registered Wi-Fi driver has no outbound WebSocket, send a short JSON hello on
|
||||||
|
UDP discovery port so the device can announce itself and we can reconnect.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
interval = float(settings.get("wifi_driver_hello_interval_s", 10.0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
interval = 10.0
|
||||||
|
if interval <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.setblocking(False)
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
if udp_holder.get("closing"):
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
dev = Device()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[hello] device list failed: {e!r}")
|
||||||
|
continue
|
||||||
|
for _mac_key, doc in list(dev.items()):
|
||||||
|
if not isinstance(doc, dict):
|
||||||
|
continue
|
||||||
|
if doc.get("transport") != "wifi":
|
||||||
|
continue
|
||||||
|
ip = _ipv4_address(str(doc.get("address") or ""))
|
||||||
|
if not ip:
|
||||||
|
continue
|
||||||
|
if tcp_client_registry.tcp_client_connected(ip):
|
||||||
|
continue
|
||||||
|
name = (doc.get("name") or "").strip()
|
||||||
|
mac = normalize_mac(doc.get("id") or _mac_key)
|
||||||
|
if not name or not mac:
|
||||||
|
continue
|
||||||
|
line = (
|
||||||
|
json.dumps(
|
||||||
|
{"m": "hello", "device_name": name, "mac": mac},
|
||||||
|
separators=(",", ":"),
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await loop.sock_sendto(
|
||||||
|
sock, line.encode("utf-8"), (ip, DISCOVERY_UDP_PORT)
|
||||||
|
)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"[hello] UDP to {ip!r} failed: {e!r}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def _run_udp_discovery_server(udp_holder=None) -> None:
|
async def _run_udp_discovery_server(udp_holder=None) -> None:
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
sock.setblocking(False)
|
sock.setblocking(False)
|
||||||
@@ -189,87 +220,6 @@ async def _run_udp_discovery_server(udp_holder=None) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def _handle_tcp_client(reader, writer):
|
|
||||||
"""Read newline-delimited JSON from Wi-Fi LED drivers; forward to serial bridge."""
|
|
||||||
peer = writer.get_extra_info("peername")
|
|
||||||
peer_ip = peer[0] if peer else ""
|
|
||||||
peer_label = f"{peer_ip}:{peer[1]}" if peer and len(peer) > 1 else peer_ip or "?"
|
|
||||||
print(f"[TCP] client connected {peer_label}")
|
|
||||||
_enable_tcp_keepalive(writer)
|
|
||||||
tcp_client_registry.register_tcp_writer(peer_ip, writer)
|
|
||||||
ping_task = asyncio.create_task(_tcp_liveness_ping_loop(writer, peer_ip))
|
|
||||||
sender = get_current_sender()
|
|
||||||
buf = b""
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
chunk = await reader.read(4096)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
raise
|
|
||||||
except _TCP_PEER_GONE as e:
|
|
||||||
print(f"[TCP] read ended ({peer_label}): {e!r}")
|
|
||||||
tcp_client_registry.unregister_tcp_writer(peer_ip, writer)
|
|
||||||
break
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
buf += chunk
|
|
||||||
while b"\n" in buf:
|
|
||||||
raw_line, buf = buf.split(b"\n", 1)
|
|
||||||
line = raw_line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
text = line.decode("utf-8")
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
print(
|
|
||||||
f"[TCP] recv {peer_label} (non-UTF-8, {len(line)} bytes): {line!r}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
print(f"[TCP] recv {peer_label}: {text}")
|
|
||||||
try:
|
|
||||||
parsed = json.loads(text)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
if sender:
|
|
||||||
try:
|
|
||||||
await sender.send(text)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
continue
|
|
||||||
if isinstance(parsed, dict):
|
|
||||||
addr = parsed.pop("to", None)
|
|
||||||
payload = json.dumps(parsed) if parsed else "{}"
|
|
||||||
if sender:
|
|
||||||
try:
|
|
||||||
await sender.send(payload, addr=addr)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"TCP forward to bridge failed: {e}")
|
|
||||||
elif sender:
|
|
||||||
try:
|
|
||||||
await sender.send(text)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
# Drop registry + broadcast connected:false before awaiting ping/close so the UI
|
|
||||||
# does not stay green if ping or wait_closed blocks on a timed-out peer.
|
|
||||||
outcome = tcp_client_registry.unregister_tcp_writer(peer_ip, writer)
|
|
||||||
if outcome == "superseded":
|
|
||||||
print(
|
|
||||||
f"[TCP] TCP session ended (same IP already has a newer connection): {peer_label}"
|
|
||||||
)
|
|
||||||
ping_task.cancel()
|
|
||||||
try:
|
|
||||||
await ping_task
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
writer.close()
|
|
||||||
await writer.wait_closed()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
raise
|
|
||||||
except _TCP_PEER_GONE:
|
|
||||||
tcp_client_registry.unregister_tcp_writer(peer_ip, writer)
|
|
||||||
|
|
||||||
|
|
||||||
async def _send_bridge_wifi_channel(settings, sender):
|
async def _send_bridge_wifi_channel(settings, sender):
|
||||||
"""Tell the serial ESP32 bridge to set STA channel (settings wifi_channel); not forwarded as ESP-NOW."""
|
"""Tell the serial ESP32 bridge to set STA channel (settings wifi_channel); not forwarded as ESP-NOW."""
|
||||||
try:
|
try:
|
||||||
@@ -285,23 +235,6 @@ async def _send_bridge_wifi_channel(settings, sender):
|
|||||||
print(f"[startup] bridge channel message failed: {e}")
|
print(f"[startup] bridge channel message failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def _run_tcp_server(settings, tcp_holder=None):
|
|
||||||
if not settings.get("tcp_enabled", True):
|
|
||||||
print("TCP server disabled (tcp_enabled=false)")
|
|
||||||
return
|
|
||||||
port = int(settings.get("tcp_port", 8765))
|
|
||||||
server = await asyncio.start_server(_handle_tcp_client, "0.0.0.0", port)
|
|
||||||
print(f"TCP server listening on 0.0.0.0:{port}")
|
|
||||||
if tcp_holder is not None:
|
|
||||||
tcp_holder["server"] = server
|
|
||||||
try:
|
|
||||||
async with server:
|
|
||||||
await server.serve_forever()
|
|
||||||
finally:
|
|
||||||
if tcp_holder is not None:
|
|
||||||
tcp_holder.pop("server", None)
|
|
||||||
|
|
||||||
|
|
||||||
async def main(port=80):
|
async def main(port=80):
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
print(settings)
|
print(settings)
|
||||||
@@ -341,6 +274,7 @@ async def main(port=80):
|
|||||||
app.mount(settings_controller.controller, '/settings')
|
app.mount(settings_controller.controller, '/settings')
|
||||||
app.mount(device_controller.controller, '/devices')
|
app.mount(device_controller.controller, '/devices')
|
||||||
|
|
||||||
|
tcp_client_registry.set_settings(settings)
|
||||||
tcp_client_registry.set_tcp_status_broadcaster(broadcast_device_tcp_status)
|
tcp_client_registry.set_tcp_status_broadcaster(broadcast_device_tcp_status)
|
||||||
|
|
||||||
# Serve index.html at root (cwd is src/ when run via pipenv run run)
|
# Serve index.html at root (cwd is src/ when run via pipenv run run)
|
||||||
@@ -407,11 +341,11 @@ async def main(port=80):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Touch Device singleton early so db/device.json exists before first TCP hello.
|
# Touch Device singleton early so db/device.json exists before first UDP hello.
|
||||||
Device()
|
Device()
|
||||||
await _send_bridge_wifi_channel(settings, sender)
|
await _send_bridge_wifi_channel(settings, sender)
|
||||||
|
_prime_wifi_outbound_driver_connections()
|
||||||
|
|
||||||
tcp_holder = {}
|
|
||||||
udp_holder = {"closing": False}
|
udp_holder = {"closing": False}
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
@@ -424,9 +358,7 @@ async def main(port=80):
|
|||||||
u.close()
|
u.close()
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
s = tcp_holder.get("server")
|
tcp_client_registry.cancel_all_driver_tasks()
|
||||||
if s is not None:
|
|
||||||
s.close()
|
|
||||||
if getattr(app, "server", None) is not None:
|
if getattr(app, "server", None) is not None:
|
||||||
app.shutdown()
|
app.shutdown()
|
||||||
|
|
||||||
@@ -439,22 +371,18 @@ async def main(port=80):
|
|||||||
except (NotImplementedError, RuntimeError):
|
except (NotImplementedError, RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Await HTTP + driver TCP together so bind failures (e.g. port 80 in use) surface
|
# Await HTTP + UDP discovery; bind failures (e.g. port 80 in use) surface here.
|
||||||
# here instead of as an unretrieved Task exception; the UI WebSocket drops if HTTP
|
|
||||||
# never starts, which clears Wi-Fi presence dots.
|
|
||||||
try:
|
try:
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
app.start_server(host="0.0.0.0", port=port),
|
app.start_server(host="0.0.0.0", port=port),
|
||||||
_run_tcp_server(settings, tcp_holder),
|
|
||||||
_run_udp_discovery_server(udp_holder),
|
_run_udp_discovery_server(udp_holder),
|
||||||
|
_periodic_wifi_driver_hello_loop(settings, udp_holder),
|
||||||
)
|
)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno == errno.EADDRINUSE:
|
if e.errno == errno.EADDRINUSE:
|
||||||
tcp_p = int(settings.get("tcp_port", 8765))
|
|
||||||
print(
|
print(
|
||||||
f"[server] bind failed (address already in use): {e!s}\n"
|
f"[server] bind failed (address already in use): {e!s}\n"
|
||||||
f"[server] HTTP is configured for port {port} (env PORT); "
|
f"[server] HTTP is configured for port {port} (env PORT). "
|
||||||
f"Wi-Fi LED drivers use tcp_port {tcp_p}. "
|
|
||||||
f"Stop the other process or use a free port, e.g. PORT=8080 pipenv run run"
|
f"Stop the other process or use a free port, e.g. PORT=8080 pipenv run run"
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -237,16 +237,19 @@ class Device(Model):
|
|||||||
"""
|
"""
|
||||||
Register or update a Wi-Fi client by **MAC** (storage id). Updates **name**,
|
Register or update a Wi-Fi client by **MAC** (storage id). Updates **name**,
|
||||||
**address** (peer IP), and optionally **type** from the client hello when valid.
|
**address** (peer IP), and optionally **type** from the client hello when valid.
|
||||||
|
|
||||||
|
Returns ``(mac_hex | None, persisted)`` where **persisted** is True iff ``save()``
|
||||||
|
ran (new row or field changes). Duplicate hellos with identical data are no-ops.
|
||||||
"""
|
"""
|
||||||
mac_hex = normalize_mac(mac)
|
mac_hex = normalize_mac(mac)
|
||||||
if not mac_hex:
|
if not mac_hex:
|
||||||
return None
|
return None, False
|
||||||
name = (device_name or "").strip()
|
name = (device_name or "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
return None
|
return None, False
|
||||||
ip = normalize_address_for_transport(peer_ip, "wifi")
|
ip = normalize_address_for_transport(peer_ip, "wifi")
|
||||||
if not ip:
|
if not ip:
|
||||||
return None
|
return None, False
|
||||||
resolved_type = None
|
resolved_type = None
|
||||||
if device_type is not None:
|
if device_type is not None:
|
||||||
try:
|
try:
|
||||||
@@ -254,7 +257,8 @@ class Device(Model):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
resolved_type = None
|
resolved_type = None
|
||||||
if mac_hex in self:
|
if mac_hex in self:
|
||||||
merged = dict(self[mac_hex])
|
prev = self[mac_hex]
|
||||||
|
merged = dict(prev)
|
||||||
merged["name"] = name
|
merged["name"] = name
|
||||||
if resolved_type is not None:
|
if resolved_type is not None:
|
||||||
merged["type"] = resolved_type
|
merged["type"] = resolved_type
|
||||||
@@ -263,9 +267,11 @@ class Device(Model):
|
|||||||
merged["transport"] = "wifi"
|
merged["transport"] = "wifi"
|
||||||
merged["address"] = ip
|
merged["address"] = ip
|
||||||
merged["id"] = mac_hex
|
merged["id"] = mac_hex
|
||||||
|
if merged == prev:
|
||||||
|
return mac_hex, False
|
||||||
self[mac_hex] = merged
|
self[mac_hex] = merged
|
||||||
self.save()
|
self.save()
|
||||||
return mac_hex
|
return mac_hex, True
|
||||||
self[mac_hex] = {
|
self[mac_hex] = {
|
||||||
"id": mac_hex,
|
"id": mac_hex,
|
||||||
"name": name,
|
"name": name,
|
||||||
@@ -276,4 +282,4 @@ class Device(Model):
|
|||||||
"zones": [],
|
"zones": [],
|
||||||
}
|
}
|
||||||
self.save()
|
self.save()
|
||||||
return mac_hex
|
return mac_hex, True
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
"""Track connected Wi-Fi LED drivers (TCP clients) for outbound JSON lines."""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
_writers = {}
|
|
||||||
|
|
||||||
|
|
||||||
def prune_stale_tcp_writers() -> None:
|
|
||||||
"""Remove writers that are already closing so the UI does not stay online."""
|
|
||||||
stale = [(ip, w) for ip, w in list(_writers.items()) if w.is_closing()]
|
|
||||||
for ip, w in stale:
|
|
||||||
unregister_tcp_writer(ip, w)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_tcp_peer_ip(ip: str) -> str:
|
|
||||||
"""Match asyncio peer addresses to registry IPs (strip IPv4-mapped IPv6 prefix)."""
|
|
||||||
s = str(ip).strip()
|
|
||||||
if s.lower().startswith("::ffff:"):
|
|
||||||
s = s[7:]
|
|
||||||
return s
|
|
||||||
# Optional ``async def (ip: str, connected: bool) -> None`` set from ``main``.
|
|
||||||
_tcp_status_broadcast = None
|
|
||||||
|
|
||||||
|
|
||||||
def set_tcp_status_broadcaster(coro) -> None:
|
|
||||||
global _tcp_status_broadcast
|
|
||||||
_tcp_status_broadcast = coro
|
|
||||||
|
|
||||||
|
|
||||||
def _schedule_tcp_status_broadcast(ip: str, connected: bool) -> None:
|
|
||||||
fn = _tcp_status_broadcast
|
|
||||||
if not fn:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
except RuntimeError:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
loop.create_task(fn(ip, connected))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def register_tcp_writer(peer_ip: str, writer) -> None:
|
|
||||||
if not peer_ip:
|
|
||||||
return
|
|
||||||
key = normalize_tcp_peer_ip(peer_ip)
|
|
||||||
if not key:
|
|
||||||
return
|
|
||||||
old = _writers.get(key)
|
|
||||||
_writers[key] = writer
|
|
||||||
_schedule_tcp_status_broadcast(key, True)
|
|
||||||
if old is not None and old is not writer:
|
|
||||||
try:
|
|
||||||
old.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def unregister_tcp_writer(peer_ip: str, writer=None) -> str:
|
|
||||||
"""
|
|
||||||
Remove the writer for peer_ip. If ``writer`` is given, only pop when it is still
|
|
||||||
the registered instance (avoids a replaced TCP session removing the new one).
|
|
||||||
|
|
||||||
Returns ``removed`` (cleared live session + UI offline), ``noop`` (already gone),
|
|
||||||
or ``superseded`` (this writer is not the registered one for that IP).
|
|
||||||
"""
|
|
||||||
if not peer_ip:
|
|
||||||
return "noop"
|
|
||||||
key = normalize_tcp_peer_ip(peer_ip)
|
|
||||||
if not key:
|
|
||||||
return "noop"
|
|
||||||
current = _writers.get(key)
|
|
||||||
if writer is not None:
|
|
||||||
if current is None:
|
|
||||||
return "noop"
|
|
||||||
if current is not writer:
|
|
||||||
return "superseded"
|
|
||||||
had = key in _writers
|
|
||||||
if had:
|
|
||||||
_writers.pop(key, None)
|
|
||||||
_schedule_tcp_status_broadcast(key, False)
|
|
||||||
print(f"[TCP] device disconnected: {key}")
|
|
||||||
return "removed"
|
|
||||||
return "noop"
|
|
||||||
|
|
||||||
|
|
||||||
def list_connected_ips():
|
|
||||||
"""IPs with an active TCP writer (for UI snapshot)."""
|
|
||||||
prune_stale_tcp_writers()
|
|
||||||
return list(_writers.keys())
|
|
||||||
|
|
||||||
|
|
||||||
def tcp_client_connected(ip: str) -> bool:
|
|
||||||
"""True if a Wi-Fi driver is connected on this IP (TCP writer registered)."""
|
|
||||||
prune_stale_tcp_writers()
|
|
||||||
key = normalize_tcp_peer_ip(ip)
|
|
||||||
return bool(key and key in _writers)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_json_line_to_ip(ip: str, json_str: str) -> bool:
|
|
||||||
"""Send one newline-terminated JSON message to a connected TCP client."""
|
|
||||||
ip = normalize_tcp_peer_ip(ip)
|
|
||||||
writer = _writers.get(ip)
|
|
||||||
if not writer:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
line = json_str if json_str.endswith("\n") else json_str + "\n"
|
|
||||||
writer.write(line.encode("utf-8"))
|
|
||||||
await writer.drain()
|
|
||||||
return True
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"[TCP] send to {ip} failed: {exc}")
|
|
||||||
unregister_tcp_writer(ip, writer)
|
|
||||||
return False
|
|
||||||
281
src/models/wifi_ws_clients.py
Normal file
281
src/models/wifi_ws_clients.py
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
"""Outbound WebSocket clients to Wi-Fi LED drivers (firmware serves ``/ws`` on device)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import errno
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
from websockets.exceptions import ConnectionClosed
|
||||||
|
|
||||||
|
_connections: dict[str, object] = {}
|
||||||
|
_send_locks: dict[str, asyncio.Lock] = {}
|
||||||
|
_tasks: dict[str, asyncio.Task] = {}
|
||||||
|
_unreachable_counts: dict[str, int] = {}
|
||||||
|
_settings = None
|
||||||
|
|
||||||
|
_tcp_status_broadcast = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_settings(settings) -> None:
|
||||||
|
global _settings
|
||||||
|
_settings = settings
|
||||||
|
|
||||||
|
|
||||||
|
def set_tcp_status_broadcaster(coro) -> None:
|
||||||
|
global _tcp_status_broadcast
|
||||||
|
_tcp_status_broadcast = coro
|
||||||
|
|
||||||
|
|
||||||
|
def _schedule_status_broadcast(ip: str, connected: bool) -> None:
|
||||||
|
fn = _tcp_status_broadcast
|
||||||
|
if not fn:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
loop.create_task(fn(ip, connected))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _benign_ws_connect_failure(exc: BaseException) -> bool:
|
||||||
|
"""True for common \"driver down / no route\" errors while dialling the WebSocket."""
|
||||||
|
if isinstance(exc, (asyncio.TimeoutError, TimeoutError)):
|
||||||
|
return True
|
||||||
|
if isinstance(exc, ConnectionRefusedError):
|
||||||
|
return True
|
||||||
|
if not isinstance(exc, OSError):
|
||||||
|
return False
|
||||||
|
en = exc.errno
|
||||||
|
if en is None:
|
||||||
|
return False
|
||||||
|
codes = {errno.ECONNREFUSED, errno.ETIMEDOUT}
|
||||||
|
for name in ("EHOSTUNREACH", "ENETUNREACH", "ENETDOWN", "EADDRNOTAVAIL"):
|
||||||
|
if hasattr(errno, name):
|
||||||
|
codes.add(getattr(errno, name))
|
||||||
|
return en in codes
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_tcp_peer_ip(ip: str) -> str:
|
||||||
|
"""Match peer addresses to registry IPs (strip IPv4-mapped IPv6 prefix)."""
|
||||||
|
s = str(ip).strip()
|
||||||
|
if s.lower().startswith("::ffff:"):
|
||||||
|
s = s[7:]
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _ws_open(ws) -> bool:
|
||||||
|
try:
|
||||||
|
return ws.close_code is None
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def prune_stale_tcp_writers() -> None:
|
||||||
|
"""Drop closed WebSocket entries (name kept for callers)."""
|
||||||
|
stale = [ip for ip, ws in list(_connections.items()) if not _ws_open(ws)]
|
||||||
|
for ip in stale:
|
||||||
|
_connections.pop(ip, None)
|
||||||
|
_schedule_status_broadcast(ip, False)
|
||||||
|
|
||||||
|
|
||||||
|
def _register_ws(ip: str, ws) -> None:
|
||||||
|
key = normalize_tcp_peer_ip(ip)
|
||||||
|
if not key:
|
||||||
|
return
|
||||||
|
_connections[key] = ws
|
||||||
|
_unreachable_counts.pop(key, None)
|
||||||
|
if key not in _send_locks:
|
||||||
|
_send_locks[key] = asyncio.Lock()
|
||||||
|
_schedule_status_broadcast(key, True)
|
||||||
|
print(f"[WS] driver connected {key!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def unregister_tcp_writer(peer_ip: str, ws=None) -> str:
|
||||||
|
"""
|
||||||
|
Remove the WebSocket for peer_ip. If ``ws`` is given, only pop when it is still
|
||||||
|
the registered instance.
|
||||||
|
|
||||||
|
Returns ``removed``, ``noop``, or ``superseded`` (same contract as former TCP registry).
|
||||||
|
"""
|
||||||
|
if not peer_ip:
|
||||||
|
return "noop"
|
||||||
|
key = normalize_tcp_peer_ip(peer_ip)
|
||||||
|
if not key:
|
||||||
|
return "noop"
|
||||||
|
current = _connections.get(key)
|
||||||
|
if ws is not None:
|
||||||
|
if current is None:
|
||||||
|
return "noop"
|
||||||
|
if current is not ws:
|
||||||
|
return "superseded"
|
||||||
|
had = key in _connections
|
||||||
|
if had:
|
||||||
|
_connections.pop(key, None)
|
||||||
|
_schedule_status_broadcast(key, False)
|
||||||
|
print(f"[WS] driver disconnected: {key}")
|
||||||
|
return "removed"
|
||||||
|
return "noop"
|
||||||
|
|
||||||
|
|
||||||
|
def list_connected_ips():
|
||||||
|
"""IPs with an active outbound WebSocket to the driver."""
|
||||||
|
prune_stale_tcp_writers()
|
||||||
|
return list(_connections.keys())
|
||||||
|
|
||||||
|
|
||||||
|
def tcp_client_connected(ip: str) -> bool:
|
||||||
|
"""True if the controller has an outbound WebSocket to this driver IP."""
|
||||||
|
prune_stale_tcp_writers()
|
||||||
|
key = normalize_tcp_peer_ip(ip)
|
||||||
|
return bool(key and key in _connections)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_json_line_to_ip(ip: str, json_str: str) -> bool:
|
||||||
|
"""Send one JSON text frame (v1 line; trailing newline stripped for WebSocket)."""
|
||||||
|
ip = normalize_tcp_peer_ip(ip)
|
||||||
|
ws = _connections.get(ip)
|
||||||
|
if ws is None or not _ws_open(ws):
|
||||||
|
return False
|
||||||
|
text = json_str.rstrip("\n")
|
||||||
|
lock = _send_locks.setdefault(ip, asyncio.Lock())
|
||||||
|
try:
|
||||||
|
async with lock:
|
||||||
|
await ws.send(text)
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[WS] send to {ip} failed: {exc}")
|
||||||
|
unregister_tcp_writer(ip, ws)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _recv_forward_loop(ip: str, ws) -> None:
|
||||||
|
from models.transport import get_current_sender
|
||||||
|
|
||||||
|
sender = get_current_sender()
|
||||||
|
async for message in ws:
|
||||||
|
if isinstance(message, bytes):
|
||||||
|
try:
|
||||||
|
text = message.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
print(f"[WS] recv {ip} (non-UTF-8, {len(message)} bytes)")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
text = message
|
||||||
|
text = text.strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
print(f"[WS] recv {ip}: {text}")
|
||||||
|
if not sender:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
parsed = json.loads(text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
try:
|
||||||
|
await sender.send(text)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
continue
|
||||||
|
if isinstance(parsed, dict):
|
||||||
|
addr = parsed.pop("to", None)
|
||||||
|
payload = json.dumps(parsed) if parsed else "{}"
|
||||||
|
try:
|
||||||
|
await sender.send(payload, addr=addr)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WS] forward to bridge failed: {e}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await sender.send(text)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def _driver_connection_loop(ip: str) -> None:
|
||||||
|
global _settings
|
||||||
|
if _settings is None:
|
||||||
|
return
|
||||||
|
port = int(_settings.get("wifi_driver_ws_port", 80))
|
||||||
|
path = str(_settings.get("wifi_driver_ws_path", "/ws"))
|
||||||
|
if not path.startswith("/"):
|
||||||
|
path = "/" + path
|
||||||
|
uri = f"ws://{ip}:{port}{path}"
|
||||||
|
retry_interval_s = 2.0
|
||||||
|
retry_window_s = 30.0
|
||||||
|
deadline = asyncio.get_running_loop().time() + retry_window_s
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
now = asyncio.get_running_loop().time()
|
||||||
|
if now >= deadline:
|
||||||
|
print(
|
||||||
|
f"[WS] driver {ip} still unreachable after {int(retry_window_s)}s; "
|
||||||
|
"stopping retries until next hello"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
print(f"[WS] connecting to {uri!r}")
|
||||||
|
async with websockets.connect(
|
||||||
|
uri,
|
||||||
|
ping_interval=20,
|
||||||
|
ping_timeout=15,
|
||||||
|
open_timeout=30,
|
||||||
|
) as ws:
|
||||||
|
_register_ws(ip, ws)
|
||||||
|
try:
|
||||||
|
await _recv_forward_loop(ip, ws)
|
||||||
|
finally:
|
||||||
|
unregister_tcp_writer(ip, ws)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except ConnectionClosed as e:
|
||||||
|
print(f"[WS] driver {ip} closed: {e}")
|
||||||
|
unregister_tcp_writer(ip, None)
|
||||||
|
except Exception as e:
|
||||||
|
if _benign_ws_connect_failure(e):
|
||||||
|
n = _unreachable_counts.get(ip, 0) + 1
|
||||||
|
_unreachable_counts[ip] = n
|
||||||
|
if n == 1 or (n % 30) == 0:
|
||||||
|
print(f"[WS] driver {ip} unreachable, retry in 2s: {e} (x{n})")
|
||||||
|
else:
|
||||||
|
print(f"[WS] driver {ip} session error: {e!r}")
|
||||||
|
traceback.print_exception(type(e), e, e.__traceback__)
|
||||||
|
_unreachable_counts.pop(ip, None)
|
||||||
|
unregister_tcp_writer(ip, None)
|
||||||
|
await asyncio.sleep(retry_interval_s)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
unregister_tcp_writer(ip, None)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
_tasks.pop(ip, None)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_driver_connection(peer_ip: str) -> None:
|
||||||
|
"""Start (or keep) a background task that maintains ``ws://<ip>:port/ws``."""
|
||||||
|
key = normalize_tcp_peer_ip(peer_ip)
|
||||||
|
if not key:
|
||||||
|
return
|
||||||
|
t = _tasks.get(key)
|
||||||
|
if t is not None and not t.done():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
return
|
||||||
|
_tasks[key] = loop.create_task(_driver_connection_loop(key))
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_all_driver_tasks() -> None:
|
||||||
|
"""Signal shutdown: cancel outbound driver connection tasks."""
|
||||||
|
for _ip, t in list(_tasks.items()):
|
||||||
|
if not t.done():
|
||||||
|
t.cancel()
|
||||||
|
_tasks.clear()
|
||||||
|
for ip in list(_connections.keys()):
|
||||||
|
_schedule_status_broadcast(ip, False)
|
||||||
|
_connections.clear()
|
||||||
|
_send_locks.clear()
|
||||||
|
_unreachable_counts.clear()
|
||||||
@@ -48,11 +48,15 @@ class Settings(dict):
|
|||||||
# ESP-NOW STA channel (2.4 GHz) for LED drivers / bridge alignment; 1–11
|
# ESP-NOW STA channel (2.4 GHz) for LED drivers / bridge alignment; 1–11
|
||||||
if 'wifi_channel' not in self:
|
if 'wifi_channel' not in self:
|
||||||
self['wifi_channel'] = 6
|
self['wifi_channel'] = 6
|
||||||
# Wi-Fi LED drivers: newline-delimited JSON over TCP (see led-driver WiFi transport)
|
# Wi-Fi LED drivers: controller opens WebSocket to device (firmware serves /ws)
|
||||||
if 'tcp_enabled' not in self:
|
if 'wifi_driver_ws_port' not in self:
|
||||||
self['tcp_enabled'] = True
|
self['wifi_driver_ws_port'] = 80
|
||||||
if 'tcp_port' not in self:
|
if 'wifi_driver_ws_path' not in self:
|
||||||
self['tcp_port'] = 8765
|
self['wifi_driver_ws_path'] = '/ws'
|
||||||
|
# Seconds between UDP discovery nudges when a Wi-Fi driver WebSocket is
|
||||||
|
# down (0 disables). Helps drivers that reconnect after seeing traffic on 8766.
|
||||||
|
if 'wifi_driver_hello_interval_s' not in self:
|
||||||
|
self['wifi_driver_hello_interval_s'] = 10.0
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Push Wi-Fi TCP connect/disconnect updates to browser WebSocket clients."""
|
"""Push Wi-Fi driver connect/disconnect updates to browser WebSocket clients."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
@@ -20,7 +20,7 @@ async def unregister_device_status_ws(ws: Any) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def broadcast_device_tcp_status(ip: str, connected: bool) -> None:
|
async def broadcast_device_tcp_status(ip: str, connected: bool) -> None:
|
||||||
from models.tcp_clients import normalize_tcp_peer_ip
|
from models.wifi_ws_clients import normalize_tcp_peer_ip
|
||||||
|
|
||||||
ip = normalize_tcp_peer_ip(ip)
|
ip = normalize_tcp_peer_ip(ip)
|
||||||
if not ip:
|
if not ip:
|
||||||
@@ -42,7 +42,7 @@ async def broadcast_device_tcp_status(ip: str, connected: bool) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def broadcast_device_tcp_snapshot_to(ws: Any) -> None:
|
async def broadcast_device_tcp_snapshot_to(ws: Any) -> None:
|
||||||
from models import tcp_clients as tcp
|
from models import wifi_ws_clients as tcp
|
||||||
|
|
||||||
ips = tcp.list_connected_ips()
|
ips = tcp.list_connected_ips()
|
||||||
msg = json.dumps({"type": "device_tcp_snapshot", "connected_ips": ips})
|
msg = json.dumps({"type": "device_tcp_snapshot", "connected_ips": ips})
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Deliver driver JSON messages over serial (ESP-NOW) and/or TCP (Wi-Fi clients)."""
|
"""Deliver driver JSON messages over serial (ESP-NOW) and/or WebSocket (Wi-Fi drivers)."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from models.device import normalize_mac
|
from models.device import normalize_mac
|
||||||
from models.tcp_clients import send_json_line_to_ip
|
from models.wifi_ws_clients import send_json_line_to_ip
|
||||||
|
|
||||||
# Serial bridge (ESP32): broadcast MAC + this envelope → firmware unicasts ``body`` to each peer.
|
# Serial bridge (ESP32): broadcast MAC + this envelope → firmware unicasts ``body`` to each peer.
|
||||||
_SPLIT_MODE = "split"
|
_SPLIT_MODE = "split"
|
||||||
@@ -20,7 +20,7 @@ def _split_serial_envelope(inner_json_str, peer_hex_list):
|
|||||||
|
|
||||||
def _wifi_message_for_device(msg, device_name):
|
def _wifi_message_for_device(msg, device_name):
|
||||||
"""
|
"""
|
||||||
For Wi-Fi TCP fanout, narrow a v1 select map to a single device name.
|
For Wi-Fi WebSocket fanout, narrow a v1 select map to a single device name.
|
||||||
Returns the original message when no narrowing applies.
|
Returns the original message when no narrowing applies.
|
||||||
"""
|
"""
|
||||||
if not device_name:
|
if not device_name:
|
||||||
@@ -40,6 +40,33 @@ def _wifi_message_for_device(msg, device_name):
|
|||||||
return json.dumps(body, separators=(",", ":"))
|
return json.dumps(body, separators=(",", ":"))
|
||||||
|
|
||||||
|
|
||||||
|
def _combine_preset_chunks_for_wifi(chunk_messages):
|
||||||
|
"""Merge chunked v1 preset messages into one v1 JSON string for Wi-Fi."""
|
||||||
|
merged_presets = {}
|
||||||
|
save_flag = False
|
||||||
|
default_id = None
|
||||||
|
for msg in chunk_messages:
|
||||||
|
try:
|
||||||
|
body = json.loads(msg)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
continue
|
||||||
|
presets = body.get("presets")
|
||||||
|
if isinstance(presets, dict):
|
||||||
|
merged_presets.update(presets)
|
||||||
|
if body.get("save"):
|
||||||
|
save_flag = True
|
||||||
|
if body.get("default") is not None:
|
||||||
|
default_id = body.get("default")
|
||||||
|
out = {"v": "1", "presets": merged_presets}
|
||||||
|
if save_flag:
|
||||||
|
out["save"] = True
|
||||||
|
if default_id is not None:
|
||||||
|
out["default"] = default_id
|
||||||
|
return json.dumps(out, separators=(",", ":"))
|
||||||
|
|
||||||
|
|
||||||
async def deliver_preset_broadcast_then_per_device(
|
async def deliver_preset_broadcast_then_per_device(
|
||||||
sender,
|
sender,
|
||||||
chunk_messages,
|
chunk_messages,
|
||||||
@@ -50,8 +77,8 @@ async def deliver_preset_broadcast_then_per_device(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Send preset definition chunks: ESP-NOW broadcast once per chunk; same chunk to each
|
Send preset definition chunks: ESP-NOW broadcast once per chunk; same chunk to each
|
||||||
Wi-Fi driver over TCP. If default_id is set, send a per-target default message
|
Wi-Fi driver over WebSocket. If default_id is set, send a per-target default message
|
||||||
(unicast serial or TCP) with targets=[device name] for each registry entry.
|
(unicast serial or WebSocket) with targets=[device name] for each registry entry.
|
||||||
"""
|
"""
|
||||||
if not chunk_messages:
|
if not chunk_messages:
|
||||||
return 0
|
return 0
|
||||||
@@ -72,17 +99,22 @@ async def deliver_preset_broadcast_then_per_device(
|
|||||||
wifi_ips.append(str(doc["address"]).strip())
|
wifi_ips.append(str(doc["address"]).strip())
|
||||||
|
|
||||||
deliveries = 0
|
deliveries = 0
|
||||||
|
wifi_combined_msg = _combine_preset_chunks_for_wifi(chunk_messages)
|
||||||
for msg in chunk_messages:
|
for msg in chunk_messages:
|
||||||
tasks = [sender.send(msg, addr=_BROADCAST_MAC_HEX)]
|
tasks = [sender.send(msg, addr=_BROADCAST_MAC_HEX)]
|
||||||
for ip in wifi_ips:
|
|
||||||
if ip:
|
|
||||||
tasks.append(send_json_line_to_ip(ip, msg))
|
|
||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
if results and results[0] is True:
|
if results and results[0] is True:
|
||||||
deliveries += 1
|
deliveries += 1
|
||||||
for r in results[1:]:
|
await asyncio.sleep(delay_s)
|
||||||
if r is True:
|
|
||||||
|
for ip in wifi_ips:
|
||||||
|
if not ip:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if await send_json_line_to_ip(ip, wifi_combined_msg):
|
||||||
deliveries += 1
|
deliveries += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[driver_delivery] Wi-Fi preset send failed: {e!r}")
|
||||||
await asyncio.sleep(delay_s)
|
await asyncio.sleep(delay_s)
|
||||||
|
|
||||||
if default_id:
|
if default_id:
|
||||||
@@ -98,7 +130,7 @@ async def deliver_preset_broadcast_then_per_device(
|
|||||||
if await send_json_line_to_ip(ip, out):
|
if await send_json_line_to_ip(ip, out):
|
||||||
deliveries += 1
|
deliveries += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[driver_delivery] default TCP failed: {e!r}")
|
print(f"[driver_delivery] default Wi-Fi send failed: {e!r}")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
await sender.send(out, addr=mac)
|
await sender.send(out, addr=mac)
|
||||||
@@ -112,10 +144,10 @@ async def deliver_preset_broadcast_then_per_device(
|
|||||||
|
|
||||||
async def deliver_json_messages(sender, messages, target_macs, devices_model, delay_s=0.1):
|
async def deliver_json_messages(sender, messages, target_macs, devices_model, delay_s=0.1):
|
||||||
"""
|
"""
|
||||||
Send each message string to the bridge and/or TCP clients.
|
Send each message string to the bridge and/or Wi-Fi WebSocket clients.
|
||||||
|
|
||||||
If target_macs is None or empty: one serial send per message (default/broadcast address).
|
If target_macs is None or empty: one serial send per message (default/broadcast address).
|
||||||
Otherwise: Wi-Fi uses TCP in parallel. Multiple ESP-NOW peers are sent in **one** serial
|
Otherwise: Wi-Fi uses WebSocket in parallel. Multiple ESP-NOW peers are sent in **one** serial
|
||||||
write to the ESP32 (broadcast + split envelope); the bridge unicasts ``body`` to each
|
write to the ESP32 (broadcast + split envelope); the bridge unicasts ``body`` to each
|
||||||
peer. A single ESP-NOW peer still uses one unicast serial frame. Wi-Fi and serial
|
peer. A single ESP-NOW peer still uses one unicast serial frame. Wi-Fi and serial
|
||||||
tasks run together in one asyncio.gather.
|
tasks run together in one asyncio.gather.
|
||||||
|
|||||||
300
tests/device_ws_cycle.py
Normal file
300
tests/device_ws_cycle.py
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Discover a Wi‑Fi LED driver via UDP hello, then drive it over WebSocket.
|
||||||
|
|
||||||
|
1. Listens on UDP (default port 8766) for the same JSON line the firmware sends
|
||||||
|
(``v``, ``device_name``, ``mac``, ``type``: ``led``).
|
||||||
|
2. Opens ``ws://<device-ip>:<port>/ws``.
|
||||||
|
3. Pushes a few test presets (``v``: ``"1"``) and cycles ``select`` for the
|
||||||
|
reported ``device_name``.
|
||||||
|
|
||||||
|
The firmware sends UDP hello about one second **after** HTTP is listening, so
|
||||||
|
this script retries the WebSocket handshake by default.
|
||||||
|
|
||||||
|
The device ``settings.json`` ``name`` must match ``device_name`` in the hello
|
||||||
|
(and in each ``select`` map).
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
pipenv install --dev
|
||||||
|
pipenv run python tests/device_ws_cycle.py
|
||||||
|
|
||||||
|
pipenv run python tests/device_ws_cycle.py --timeout 60 --cycle-s 4
|
||||||
|
|
||||||
|
# Skip UDP; connect directly (set ``--device-name`` to the device's ``name``)::
|
||||||
|
pipenv run python tests/device_ws_cycle.py --host 192.168.1.42 --device-name a
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_hello_line(data: bytes) -> tuple[dict | None, bytes]:
|
||||||
|
line = data.split(b"\n", 1)[0].strip()
|
||||||
|
if not line:
|
||||||
|
return None, line
|
||||||
|
try:
|
||||||
|
obj = json.loads(line.decode("utf-8"))
|
||||||
|
except (UnicodeError, ValueError, TypeError):
|
||||||
|
return None, line
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return None, line
|
||||||
|
return obj, line
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_udp_hello(
|
||||||
|
bind: str,
|
||||||
|
port: int,
|
||||||
|
timeout_s: float,
|
||||||
|
echo: bool,
|
||||||
|
) -> tuple[str, str, dict]:
|
||||||
|
"""Block until a valid hello arrives. Returns (device_ip, device_name, hello_dict)."""
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
sock.bind((bind, port))
|
||||||
|
sock.settimeout(timeout_s)
|
||||||
|
print(
|
||||||
|
f"UDP listening on {bind}:{port} (timeout {timeout_s}s) — "
|
||||||
|
"power the device or wait for hello…",
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data, addr = sock.recvfrom(2048)
|
||||||
|
except socket.timeout as e:
|
||||||
|
raise SystemExit(f"No UDP hello before timeout: {e}") from e
|
||||||
|
peer_ip = addr[0]
|
||||||
|
parsed, raw_line = _parse_hello_line(data)
|
||||||
|
if parsed is None:
|
||||||
|
print(f"Ignored datagram from {peer_ip!r}: {raw_line!r}")
|
||||||
|
continue
|
||||||
|
if str(parsed.get("v") or "") != "1":
|
||||||
|
print(f"Ignored v={parsed.get('v')!r} from {peer_ip!r}")
|
||||||
|
continue
|
||||||
|
dev_type = parsed.get("type") or parsed.get("device_type")
|
||||||
|
if dev_type is not None and dev_type != "led":
|
||||||
|
print(f"Ignored type={dev_type!r} from {peer_ip!r}")
|
||||||
|
continue
|
||||||
|
name = str(parsed.get("device_name") or "").strip()
|
||||||
|
mac = parsed.get("mac")
|
||||||
|
if not name or not mac:
|
||||||
|
print(
|
||||||
|
f"Ignored hello without device_name/mac from {peer_ip!r}: {parsed!r}",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
print(
|
||||||
|
f"Heard hello: ip={peer_ip!r} device_name={name!r} mac={mac!r}",
|
||||||
|
)
|
||||||
|
if echo:
|
||||||
|
try:
|
||||||
|
sock.sendto(data, addr)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"UDP echo to {addr} failed: {e!r}")
|
||||||
|
return peer_ip, name, parsed
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
PRESETS = {
|
||||||
|
"_test_on": {"p": "on", "c": [(0, 80, 200)]},
|
||||||
|
"_test_blink": {"p": "blink", "d": 120, "b": 200, "c": [(255, 40, 0), (0, 40, 255)]},
|
||||||
|
"_test_rainbow": {"p": "rainbow", "d": 12, "n1": 2, "a": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
PRESET_ORDER = ["_test_on", "_test_blink", "_test_rainbow"]
|
||||||
|
|
||||||
|
|
||||||
|
async def cycle_presets(
|
||||||
|
host: str,
|
||||||
|
device_name: str,
|
||||||
|
ws_port: int,
|
||||||
|
ws_path: str,
|
||||||
|
cycle_s: float,
|
||||||
|
passes: int,
|
||||||
|
*,
|
||||||
|
ws_open_timeout_s: float,
|
||||||
|
ws_connect_retries: int,
|
||||||
|
ws_connect_retry_delay_s: float,
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
import websockets
|
||||||
|
except ImportError as e:
|
||||||
|
raise SystemExit(
|
||||||
|
"Install websockets: pipenv install websockets (or: pip install websockets)"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
path = ws_path if ws_path.startswith("/") else "/" + ws_path
|
||||||
|
uri = f"ws://{host}:{ws_port}{path}"
|
||||||
|
print(f"WebSocket connect {uri!r} …")
|
||||||
|
|
||||||
|
n = max(1, ws_connect_retries)
|
||||||
|
last_err: BaseException | None = None
|
||||||
|
for attempt in range(n):
|
||||||
|
try:
|
||||||
|
async with websockets.connect(
|
||||||
|
uri,
|
||||||
|
open_timeout=ws_open_timeout_s,
|
||||||
|
) as ws:
|
||||||
|
print("Connected.")
|
||||||
|
push = json.dumps({"v": "1", "presets": PRESETS})
|
||||||
|
await ws.send(push)
|
||||||
|
print(f"Sent presets: {list(PRESETS.keys())}")
|
||||||
|
await asyncio.sleep(0.25)
|
||||||
|
|
||||||
|
for p in range(passes):
|
||||||
|
print(f"--- pass {p + 1}/{passes} ---")
|
||||||
|
for pname in PRESET_ORDER:
|
||||||
|
sel = json.dumps({"v": "1", "select": {device_name: [pname]}})
|
||||||
|
await ws.send(sel)
|
||||||
|
print(f" select {pname!r}")
|
||||||
|
await asyncio.sleep(cycle_s)
|
||||||
|
|
||||||
|
print("Done.")
|
||||||
|
return
|
||||||
|
except (TimeoutError, OSError, ConnectionError) as e:
|
||||||
|
last_err = e
|
||||||
|
if attempt + 1 < n:
|
||||||
|
print(
|
||||||
|
f" connect failed ({e!r}), retry {attempt + 2}/{n} in "
|
||||||
|
f"{ws_connect_retry_delay_s}s …",
|
||||||
|
)
|
||||||
|
await asyncio.sleep(ws_connect_retry_delay_s)
|
||||||
|
|
||||||
|
raise TimeoutError(
|
||||||
|
f"WebSocket handshake failed after {n} attempts: {last_err!r}",
|
||||||
|
) from last_err
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="UDP hello discovery + WebSocket preset cycle (led-driver)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--bind",
|
||||||
|
default="0.0.0.0",
|
||||||
|
help="UDP bind address (default 0.0.0.0)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--udp-port",
|
||||||
|
type=int,
|
||||||
|
default=8766,
|
||||||
|
help="UDP listen port (default 8766)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
type=float,
|
||||||
|
default=120.0,
|
||||||
|
help="Seconds to wait for first hello (default 120)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-echo",
|
||||||
|
action="store_true",
|
||||||
|
help="Do not echo the datagram back (firmware often uses wait_reply=False)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--host",
|
||||||
|
default="",
|
||||||
|
metavar="IP",
|
||||||
|
help="Skip UDP and use this device IP",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--device-name",
|
||||||
|
default="",
|
||||||
|
metavar="NAME",
|
||||||
|
help="Device settings name for select map (required with --host if not default)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ws-port",
|
||||||
|
type=int,
|
||||||
|
default=80,
|
||||||
|
help="Device WebSocket port (default 80)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ws-path",
|
||||||
|
default="/ws",
|
||||||
|
help="WebSocket path (default /ws)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--cycle-s",
|
||||||
|
type=float,
|
||||||
|
default=3.0,
|
||||||
|
help="Seconds between select commands (default 3)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--passes",
|
||||||
|
type=int,
|
||||||
|
default=2,
|
||||||
|
help="How many full cycles through all test presets (default 2)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ws-open-timeout",
|
||||||
|
type=float,
|
||||||
|
default=30.0,
|
||||||
|
help="Per-attempt WebSocket handshake timeout in seconds (default 30)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ws-retries",
|
||||||
|
type=int,
|
||||||
|
default=15,
|
||||||
|
help="WebSocket connect attempts (default 15; use with device hello after HTTP)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ws-retry-delay",
|
||||||
|
type=float,
|
||||||
|
default=1.0,
|
||||||
|
help="Seconds between WebSocket retries (default 1)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.host:
|
||||||
|
host = args.host.strip()
|
||||||
|
device_name = (args.device_name or "a").strip()
|
||||||
|
if not device_name:
|
||||||
|
print("--device-name is required when using a generic --host", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"Using host {host!r} device_name {device_name!r} (no UDP)")
|
||||||
|
else:
|
||||||
|
host, device_name, _hello = wait_for_udp_hello(
|
||||||
|
args.bind,
|
||||||
|
args.udp_port,
|
||||||
|
args.timeout,
|
||||||
|
echo=not args.no_echo,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run(
|
||||||
|
cycle_presets(
|
||||||
|
host=host,
|
||||||
|
device_name=device_name,
|
||||||
|
ws_port=args.ws_port,
|
||||||
|
ws_path=args.ws_path,
|
||||||
|
cycle_s=args.cycle_s,
|
||||||
|
passes=max(1, args.passes),
|
||||||
|
ws_open_timeout_s=args.ws_open_timeout,
|
||||||
|
ws_connect_retries=args.ws_retries,
|
||||||
|
ws_connect_retry_delay_s=args.ws_retry_delay,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nInterrupted.")
|
||||||
|
return 130
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -93,39 +93,43 @@ def test_device():
|
|||||||
|
|
||||||
def test_upsert_wifi_tcp_client():
|
def test_upsert_wifi_tcp_client():
|
||||||
devices = _fresh_device()
|
devices = _fresh_device()
|
||||||
assert devices.upsert_wifi_tcp_client("", "192.168.1.10", None) is None
|
assert devices.upsert_wifi_tcp_client("", "192.168.1.10", None) == (None, False)
|
||||||
assert devices.upsert_wifi_tcp_client("kitchen", "192.168.1.20", "bad") is None
|
assert devices.upsert_wifi_tcp_client("kitchen", "192.168.1.20", "bad") == (
|
||||||
|
None,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
m1 = "001122334455"
|
m1 = "001122334455"
|
||||||
m2 = "001122334466"
|
m2 = "001122334466"
|
||||||
i1 = devices.upsert_wifi_tcp_client("kitchen", "192.168.1.20", m1)
|
i1, p1 = devices.upsert_wifi_tcp_client("kitchen", "192.168.1.20", m1)
|
||||||
assert i1 == m1
|
assert i1 == m1 and p1 is True
|
||||||
d = devices.read(i1)
|
d = devices.read(i1)
|
||||||
assert d["name"] == "kitchen"
|
assert d["name"] == "kitchen"
|
||||||
assert d["type"] == "led"
|
assert d["type"] == "led"
|
||||||
assert d["transport"] == "wifi"
|
assert d["transport"] == "wifi"
|
||||||
assert d["address"] == "192.168.1.20"
|
assert d["address"] == "192.168.1.20"
|
||||||
|
|
||||||
i2 = devices.upsert_wifi_tcp_client("kitchen", "192.168.1.21", m2)
|
noop_mac, noop_p = devices.upsert_wifi_tcp_client("kitchen", "192.168.1.20", m1)
|
||||||
assert i2 == m2
|
assert noop_mac == m1 and noop_p is False
|
||||||
|
|
||||||
|
i2, p2 = devices.upsert_wifi_tcp_client("kitchen", "192.168.1.21", m2)
|
||||||
|
assert i2 == m2 and p2 is True
|
||||||
assert devices.read(m1)["address"] == "192.168.1.20"
|
assert devices.read(m1)["address"] == "192.168.1.20"
|
||||||
assert devices.read(m2)["address"] == "192.168.1.21"
|
assert devices.read(m2)["address"] == "192.168.1.21"
|
||||||
assert devices.read(m1)["name"] == devices.read(m2)["name"] == "kitchen"
|
assert devices.read(m1)["name"] == devices.read(m2)["name"] == "kitchen"
|
||||||
|
|
||||||
again = devices.upsert_wifi_tcp_client("kitchen", "192.168.1.99", m1)
|
again, p_again = devices.upsert_wifi_tcp_client("kitchen", "192.168.1.99", m1)
|
||||||
assert again == m1
|
assert again == m1 and p_again is True
|
||||||
assert devices.read(m1)["address"] == "192.168.1.99"
|
assert devices.read(m1)["address"] == "192.168.1.99"
|
||||||
|
|
||||||
assert (
|
bogus_mac, bogus_p = devices.upsert_wifi_tcp_client(
|
||||||
devices.upsert_wifi_tcp_client(
|
"kitchen", "192.168.1.100", m1, device_type="bogus"
|
||||||
"kitchen", "192.168.1.100", m1, device_type="bogus"
|
|
||||||
)
|
|
||||||
== m1
|
|
||||||
)
|
)
|
||||||
|
assert bogus_mac == m1 and bogus_p is True
|
||||||
assert devices.read(m1)["type"] == "led"
|
assert devices.read(m1)["type"] == "led"
|
||||||
|
|
||||||
i3 = devices.upsert_wifi_tcp_client("hall", "10.0.0.5", "deadbeefcafe")
|
i3, p3 = devices.upsert_wifi_tcp_client("hall", "10.0.0.5", "deadbeefcafe")
|
||||||
assert i3 == "deadbeefcafe"
|
assert i3 == "deadbeefcafe" and p3 is True
|
||||||
assert len(devices.list()) == 3
|
assert len(devices.list()) == 3
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user