diff --git a/Pipfile b/Pipfile index 118d257..6c34fc8 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,7 @@ wyze-sdk = {path = "."} requests = "*" blackboxprotobuf = "*" mintotp = "*" +pycryptodomex = "*" [dev-packages] flake8 = "*" diff --git a/Pipfile.lock b/Pipfile.lock index d12c23a..380b3ff 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "36bea93df3455b92dbf8ad7dc01554a416e5fe30c7e718a7f2c53864051409e7" + "sha256": "9ef5814d402be7e16dd0c6150c6757611768ebab9c9f3cce0c7c683290edb06d" }, "pipfile-spec": 6, "requires": { @@ -25,30 +25,27 @@ }, "certifi": { "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", + "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" ], - "version": "==2020.12.5" + "markers": "python_version >= '3.6'", + "version": "==2022.9.24" }, - "chardet": { + "charset-normalizer": { "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.0.0" + "markers": "python_version >= '3.6'", + "version": "==2.1.1" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" - }, - "iniconfig": { - "path": ".", - "version": "==1.1.5" + "markers": "python_version >= '3.5'", + "version": "==3.4" }, "mintotp": { "hashes": [ @@ -80,13 +77,57 @@ ], "version": "==3.10.0" }, + "pycryptodomex": { + "hashes": [ + "sha256:04cc393045a8f19dd110c975e30f38ed7ab3faf21ede415ea67afebd95a22380", + "sha256:0776bfaf2c48154ab54ea45392847c1283d2fcf64e232e85565f858baedfc1fa", + "sha256:0fadb9f7fa3150577800eef35f62a8a24b9ddf1563ff060d9bd3af22d3952c8c", + "sha256:18e2ab4813883ae63396c0ffe50b13554b32bb69ec56f0afaf052e7a7ae0d55b", + "sha256:191e73bc84a8064ad1874dba0ebadedd7cce4dedee998549518f2c74a003b2e1", + "sha256:35a8f7afe1867118330e2e0e0bf759c409e28557fb1fc2fbb1c6c937297dbe9a", + "sha256:3709f13ca3852b0b07fc04a2c03b379189232b24007c466be0f605dd4723e9d4", + "sha256:4540904c09704b6f831059c0dfb38584acb82cb97b0125cd52688c1f1e3fffa6", + "sha256:463119d7d22d0fc04a0f9122e9d3e6121c6648bcb12a052b51bd1eed1b996aa2", + "sha256:46b3f05f2f7ac7841053da4e0f69616929ca3c42f238c405f6c3df7759ad2780", + "sha256:48697790203909fab02a33226fda546604f4e2653f9d47bc5d3eb40879fa7c64", + "sha256:5676a132169a1c1a3712edf25250722ebc8c9102aa9abd814df063ca8362454f", + "sha256:65204412d0c6a8e3c41e21e93a5e6054a74fea501afa03046a388cf042e3377a", + "sha256:67e1e6a92151023ccdfcfbc0afb3314ad30080793b4c27956ea06ab1fb9bcd8a", + "sha256:6f5b6ba8aefd624834bc177a2ac292734996bb030f9d1b388e7504103b6fcddf", + "sha256:7341f1bb2dadb0d1a0047f34c3a58208a92423cdbd3244d998e4b28df5eac0ed", + "sha256:78d9621cf0ea35abf2d38fa2ca6d0634eab6c991a78373498ab149953787e5e5", + "sha256:8eecdf9cdc7343001d047f951b9cc805cd68cb6cd77b20ea46af5bffc5bd3dfb", + "sha256:94c7b60e1f52e1a87715571327baea0733708ab4723346598beca4a3b6879794", + "sha256:996e1ba717077ce1e6d4849af7a1426f38b07b3d173b879e27d5e26d2e958beb", + "sha256:a07a64709e366c2041cd5cfbca592b43998bf4df88f7b0ca73dca37071ccf1bd", + "sha256:b6306403228edde6e289f626a3908a2f7f67c344e712cf7c0a508bab3ad9e381", + "sha256:b9279adc16e4b0f590ceff581f53a80179b02cba9056010d733eb4196134a870", + "sha256:c4cb9cb492ea7dcdf222a8d19a1d09002798ea516aeae8877245206d27326d86", + "sha256:dd452a5af7014e866206d41751886c9b4bf379a339fdf2dbfc7dd16c0fb4f8e0", + "sha256:e2b12968522a0358b8917fc7b28865acac002f02f4c4c6020fcb264d76bfd06d", + "sha256:e3164a18348bd53c69b4435ebfb4ac8a4076291ffa2a70b54f0c4b80c7834b1d", + "sha256:e47bf8776a7e15576887f04314f5228c6527b99946e6638cf2f16da56d260cab", + "sha256:f8be976cec59b11f011f790b88aca67b4ea2bd286578d0bd3e31bcd19afcd3e4", + "sha256:fc9bc7a9b79fe5c750fc81a307052f8daabb709bdaabb0fb18fb136b66b653b5" + ], + "index": "pypi", + "version": "==3.15.0" + }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" ], "index": "pypi", - "version": "==2.25.1" + "version": "==2.28.1" + }, + "setuptools": { + "hashes": [ + "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012", + "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e" + ], + "markers": "python_version >= '3.7'", + "version": "==65.4.1" }, "six": { "hashes": [ @@ -98,14 +139,15 @@ }, "urllib3": { "hashes": [ - "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", - "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "version": "==1.26.12" }, "wyze-sdk": { - "path": "." + "path": ".", + "version": "==1.5.0" } }, "develop": { @@ -118,191 +160,207 @@ }, "babel": { "hashes": [ - "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9", - "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0" + "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51", + "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.1" + "markers": "python_version >= '3.6'", + "version": "==2.10.3" }, "certifi": { "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", + "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" ], - "version": "==2020.12.5" + "markers": "python_version >= '3.6'", + "version": "==2022.9.24" }, - "chardet": { + "charset-normalizer": { "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.0.0" + "markers": "python_version >= '3.6'", + "version": "==2.1.1" }, "docutils": { "hashes": [ - "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", - "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" + "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6", + "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.17.1" + "markers": "python_version >= '3.7'", + "version": "==0.19" }, "flake8": { "hashes": [ - "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", - "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" + "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", + "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248" ], "index": "pypi", - "version": "==3.9.2" + "version": "==5.0.4" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" + "markers": "python_version >= '3.5'", + "version": "==3.4" }, "imagesize": { "hashes": [ - "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", - "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" + "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", + "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.2.0" + "version": "==1.4.1" + }, + "importlib-metadata": { + "hashes": [ + "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab", + "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43" + ], + "markers": "python_version < '3.10'", + "version": "==5.0.0" }, "isort": { "hashes": [ - "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", - "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" + "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", + "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" ], "index": "pypi", - "version": "==5.8.0" + "version": "==5.10.1" }, "jinja2": { "hashes": [ - "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", - "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "markers": "python_version >= '3.7'", + "version": "==3.1.2" }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", + "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", + "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", + "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", + "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", + "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", + "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", + "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", + "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", + "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", + "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", + "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", + "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", + "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", + "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", + "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", + "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", + "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", + "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", + "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", + "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", + "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", + "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", + "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", + "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", + "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", + "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", + "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", + "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", + "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", + "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", + "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", + "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", + "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", + "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", + "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", + "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", + "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", + "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", + "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.1" }, "mccabe": { "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" ], - "version": "==0.6.1" + "markers": "python_version >= '3.6'", + "version": "==0.7.0" }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.9" + "markers": "python_version >= '3.6'", + "version": "==21.3" }, "pycodestyle": { "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", + "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.7.0" + "markers": "python_version >= '3.6'", + "version": "==2.9.1" }, "pyflakes": { "hashes": [ - "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", - "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" + "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", + "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.3.1" + "markers": "python_version >= '3.6'", + "version": "==2.5.0" }, "pygments": { "hashes": [ - "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", - "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" + "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1", + "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42" ], - "markers": "python_version >= '3.5'", - "version": "==2.9.0" + "markers": "python_version >= '3.6'", + "version": "==2.13.0" }, "pyparsing": { "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.9" }, "pytz": { "hashes": [ - "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", - "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91", + "sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174" ], - "version": "==2021.1" + "version": "==2022.4" }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" ], "index": "pypi", - "version": "==2.25.1" + "version": "==2.28.1" }, "snowballstemmer": { "hashes": [ - "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", - "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" + "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" ], - "version": "==2.1.0" + "version": "==2.2.0" }, "sphinx": { "hashes": [ - "sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c", - "sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4" + "sha256:5b10cb1022dac8c035f75767799c39217a05fc0fe2d6fe5597560d38e44f0363", + "sha256:7abf6fabd7b58d0727b7317d5e2650ef68765bbe0ccb63c8795fa8683477eaa2" ], "index": "pypi", - "version": "==4.0.2" + "version": "==5.2.3" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -322,11 +380,11 @@ }, "sphinxcontrib-htmlhelp": { "hashes": [ - "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", - "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" + "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07", + "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2" ], - "markers": "python_version >= '3.5'", - "version": "==1.0.3" + "markers": "python_version >= '3.6'", + "version": "==2.0.0" }, "sphinxcontrib-jsmath": { "hashes": [ @@ -346,19 +404,27 @@ }, "sphinxcontrib-serializinghtml": { "hashes": [ - "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", - "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" + "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", + "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" ], "markers": "python_version >= '3.5'", - "version": "==1.1.4" + "version": "==1.1.5" }, "urllib3": { "hashes": [ - "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", - "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "version": "==1.26.12" + }, + "zipp": { + "hashes": [ + "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb", + "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.4" + "markers": "python_version >= '3.7'", + "version": "==3.9.0" } } } diff --git a/wyze_sdk/api/client.py b/wyze_sdk/api/client.py index 0b039b1..915caf8 100644 --- a/wyze_sdk/api/client.py +++ b/wyze_sdk/api/client.py @@ -88,7 +88,7 @@ def motion_sensors(self) -> MotionSensorsClient: @property def locks(self) -> LocksClient: - return LocksClient(token=self._token, base_url=self._base_url) + return LocksClient(token=self._token, base_url=self._base_url, user_id=self._user_id) @property def scales(self) -> ScalesClient: diff --git a/wyze_sdk/api/devices/locks.py b/wyze_sdk/api/devices/locks.py index 53ac2b1..10f1d56 100644 --- a/wyze_sdk/api/devices/locks.py +++ b/wyze_sdk/api/devices/locks.py @@ -1,11 +1,23 @@ from abc import ABCMeta -from datetime import datetime +from datetime import datetime, timedelta +import re from typing import Optional, Sequence from wyze_sdk.api.base import BaseClient +from wyze_sdk.errors import WyzeRequestError from wyze_sdk.models.devices import DeviceModels, Lock, LockGateway -from wyze_sdk.models.devices.locks import LockRecord +from wyze_sdk.models.devices.locks import LockKey, LockKeyPeriodicity, LockKeyPermission, LockKeyPermissionType, LockKeyType, LockRecord from wyze_sdk.service import FordServiceClient, WyzeResponse +from wyze_sdk.signature import CBCEncryptor, MD5Hasher + +# The relationship between locks and gateways is a bit complicated. +# Gateways can supposedly service multiple locks, with each lock +# being paired with exactly one gateway. The gateway is accessible +# from the Wyze app, but it really only exists there to modify WiFi +# network information in the event that it changes. +# +# Keypads are paired 1:1 with locks, with the lock entity storing +# the relationship key. class BaseLockClient(BaseClient, metaclass=ABCMeta): @@ -28,7 +40,7 @@ def _list_lock_gateways(self, **kwargs) -> Sequence[dict]: class LockGatewaysClient(BaseLockClient): def list(self, **kwargs) -> Sequence[LockGateway]: - """Lists all lock gateway available to a Wyze account. + """Lists all lock gateways available to a Wyze account. :rtype: Sequence[LockGateway] """ @@ -117,7 +129,18 @@ def get_records(self, *, device_mac: str, limit: int = 20, since: datetime, unti :rtype: Sequence[LockRecord] """ - return [LockRecord(**record) for record in super()._ford_client().get_family_record(uuid=Lock.parse_uuid(mac=device_mac), begin=since, end=until, limit=limit, offset=offset)["family_record"]] + return [LockRecord(**record) for record in super()._ford_client().get_family_records(uuid=Lock.parse_uuid(mac=device_mac), begin=since, end=until, limit=limit, offset=offset)["family_record"]] + + def get_keys(self, *, device_mac: str, **kwargs) -> Sequence[LockKey]: + """Retrieves keys for a lock. + + Args: + :param str device_mac: The device mac. e.g. ``ABCDEF1234567890`` + + :rtype: Sequence[LockKey] + """ + uuid = Lock.parse_uuid(mac=device_mac) + return [LockKey(type=LockKeyType.ACCESS_CODE, **password) for password in super()._ford_client().get_passwords(uuid=uuid)["passwords"]] def info(self, *, device_mac: str, **kwargs) -> Optional[Lock]: """Retrieves details of a lock. @@ -150,6 +173,77 @@ def info(self, *, device_mac: str, **kwargs) -> Optional[Lock]: return Lock(**lock) + def _validate_access_code(self, access_code: str): + if access_code is None or access_code.strip() == '': + raise WyzeRequestError("access code must be a numeric code between 4 and 8 digits long") + if re.match('\d{4,8}$', access_code) is None: + raise WyzeRequestError(f"{access_code} is not a valid access code") + + def _encrypt_access_code(self, access_code: str) -> str: + secret = self._ford_client().get_crypt_secret()["secret"] + return CBCEncryptor(self._ford_client().WYZE_FORD_IV_HEX).encrypt(MD5Hasher().hash(secret), access_code).hex() + + def create_access_code(self, device_mac: str, access_code: str, name: Optional[str], permission: Optional[LockKeyPermission] = None, periodicity: Optional[LockKeyPeriodicity] = None, **kwargs) -> WyzeResponse: + """Creates a guest access code on a lock. + + :param str device_mac: The device mac. e.g. ``ABCDEF1234567890`` + :param str access_code: The new access code. e.g. ``1234`` + :param str name: The name for the guest access code. + :param LockKeyPermission permission: The access permission rules for the guest access code. + :param Optional[LockKeyPeriodicity] periodicity: The recurrance rules for a recurring guest access code. + + :rtype: WyzeResponse + + :raises WyzeRequestError: if the new access code is not valid + """ + self._validate_access_code(access_code=access_code) + if permission.type == LockKeyPermissionType.RECURRING and periodicity is None: + raise WyzeRequestError("periodicity must be provided when setting recurring permission") + if permission.type == LockKeyPermissionType.ONCE: + if permission.begin is None: + permission.begin = datetime.now() + if permission.end is None: + permission.end = permission.begin + timedelta(days=30) + if permission is None: + permission = LockKeyPermission(type=LockKeyPermissionType.ALWAYS) + + uuid = Lock.parse_uuid(mac=device_mac) + return self._ford_client().add_password(uuid=uuid, password=self._encrypt_access_code(access_code=access_code), name=name, permission=permission, periodicity=periodicity, userid=self._user_id) + + def delete_access_code(self, device_mac: str, access_code_id: int, **kwargs) -> WyzeResponse: + """Deletes an access code from a lock. + + :param str device_mac: The device mac. e.g. ``ABCDEF1234567890`` + :param int access_code_id: The id of the access code to delete. + + :rtype: WyzeResponse + """ + uuid = Lock.parse_uuid(mac=device_mac) + return self._ford_client().delete_password(uuid=uuid, password_id=str(access_code_id)) + + def update_access_code(self, device_mac: str, access_code_id: int, access_code: Optional[str] = None, name: Optional[str] = None, permission: LockKeyPermission = None, periodicity: Optional[LockKeyPeriodicity] = None, **kwargs) -> WyzeResponse: + """Updates an existing access code on a lock. + + :param str device_mac: The device mac. e.g. ``ABCDEF1234567890`` + :param int access_code_id: The id of the access code to reset. + :param Optional[str] access_code: The new access code. e.g. ``1234`` + :param Optional[str] name: The new name for the guest access code. + :param LockKeyPermission permission: The access permission rules for the guest access code. + :param Optional[LockKeyPeriodicity] periodicity: The recurrance rules for a recurring guest access code. + + :rtype: WyzeResponse + + :raises WyzeRequestError: if the new access code is not valid + """ + self._validate_access_code(access_code=access_code) + if permission is None: + raise WyzeRequestError("permission must be provided") + if permission.type == LockKeyPermissionType.RECURRING and periodicity is None: + raise WyzeRequestError("periodicity must be provided when setting recurring permission") + + uuid = Lock.parse_uuid(mac=device_mac) + return self._ford_client().update_password(uuid=uuid, password_id=str(access_code_id), password=self._encrypt_access_code(access_code=access_code), name=name, permission=permission, periodicity=periodicity) + @property def gateways(self) -> LockGatewaysClient: """Returns a lock gateway client. diff --git a/wyze_sdk/models/__init__.py b/wyze_sdk/models/__init__.py index 92e806e..c6718e0 100644 --- a/wyze_sdk/models/__init__.py +++ b/wyze_sdk/models/__init__.py @@ -5,7 +5,7 @@ import distutils.util import logging from abc import ABCMeta, abstractmethod -from datetime import datetime +from datetime import datetime, time from functools import wraps from typing import Any, Callable, Iterable, Optional, Sequence, Set, Union @@ -29,6 +29,19 @@ def epoch_to_datetime(epoch: Union[int, float], ms: bool = False) -> datetime: return datetime.fromtimestamp(float(epoch) / 1000 if ms else float(epoch)) +def str_to_time(string: Union[int, str]) -> Optional[time]: + """ + Convert a string representation to a python time. + """ + if isinstance(string, int): + string = str(string) + + if len(string) != 6: + return + + return time(hour=int(string[0:2]), minute=int(string[2:4]), second=int(string[4:6])) + + def show_unknown_key_warning(name: Union[str, object], others: dict): if "type" in others: others.pop("type") diff --git a/wyze_sdk/models/devices/base.py b/wyze_sdk/models/devices/base.py index 8872dab..032849c 100644 --- a/wyze_sdk/models/devices/base.py +++ b/wyze_sdk/models/devices/base.py @@ -501,11 +501,11 @@ class LockableMixin(metaclass=ABCMeta): @property def is_locked(self) -> bool: - return self.lock_state + return False @property - def lock_state(self) -> bool: - return False if self._lock_state is None else self._lock_state.value + def lock_state(self) -> DeviceProp: + return self._lock_state @lock_state.setter def lock_state(self, value: DeviceProp): diff --git a/wyze_sdk/models/devices/locks.py b/wyze_sdk/models/devices/locks.py index 9311540..912bceb 100644 --- a/wyze_sdk/models/devices/locks.py +++ b/wyze_sdk/models/devices/locks.py @@ -1,16 +1,19 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, time from enum import Enum from typing import Optional, Sequence, Set, Tuple, Union from wyze_sdk.models import (JsonObject, PropDef, epoch_to_datetime, - show_unknown_key_warning) + show_unknown_key_warning, str_to_time) from .base import (AbstractWirelessNetworkedDevice, ContactMixin, Device, DeviceModels, DeviceProp, LockableMixin, VoltageMixin) +# door_open_status and notice in device_params appear to be unused +# notifications are controlled by a different API +# see: https://wyze-lock-service-broker.wyzecam.com/app/v2/lock class LockProps(object): """ :meta private: @@ -18,11 +21,11 @@ class LockProps(object): @classmethod def locker_lock_state(cls) -> PropDef: - return PropDef("hardlock", bool, int) + return PropDef("hardlock", int, acceptable_values=range(-1, 6)) @classmethod def locker_open_close_state(cls) -> PropDef: - return PropDef("door", bool, int) + return PropDef("door", int, acceptable_values=[1, 2]) @classmethod def lock_state(cls) -> PropDef: @@ -32,10 +35,73 @@ def lock_state(cls) -> PropDef: def open_close_state(cls) -> PropDef: return PropDef("open_close_state", bool, int) + @classmethod + def onoff_line(cls) -> PropDef: + return PropDef("onoff_line", bool, int) + @classmethod def voltage(cls) -> PropDef: return PropDef("power", int) + @classmethod + def ajar_alarm(cls) -> PropDef: + return PropDef("ajar_alarm", int, acceptable_values=[1, 2]) + + @classmethod + def trash_mode(cls) -> PropDef: + return PropDef("trash_mode", int, acceptable_values=[1, 2]) + + @classmethod + def auto_unlock(cls) -> PropDef: + return PropDef("auto_unlock", int, acceptable_values=[1, 2]) + + @classmethod + def door_sensor(cls) -> PropDef: + return PropDef("door_sensor", int, acceptable_values=[1, 2]) + + @classmethod + def auto_lock_time(cls) -> PropDef: + return PropDef("auto_lock_time", int, acceptable_values=range(0, 7)) + + @classmethod + def left_open_time(cls) -> PropDef: + return PropDef("left_open_time", int, acceptable_values=range(0, 7)) + + @classmethod + def open_volume(cls) -> PropDef: + return PropDef("open_volume", int, acceptable_values=range(0, 100)) + + @classmethod + def keypad_enable_status(cls) -> PropDef: + return PropDef("keypad_enable_status", int, acceptable_values=[1, 2]) + + +class LockStatusType(Enum): + """ + See: com.yunding.ford.widget.LockStatusWidget + """ + + OFFLINE = ('Offline', -1) + CONNECTING = ('Connecting', 0) + LOCKED = ('Locked', 1) + LOCKING = ('Locking', 2) + UNLOCKED = ('Unlocked', 3) + UNLOCKING = ('Unlocking', 4) + UNCALIBRATED = ('Uncalibrated', 5) + + def __init__(self, description: str, code: int): + self.description = description + self.code = code + + def describe(self): + return self.description + + @classmethod + def parse(cls, code: int) -> Optional["LockStatusType"]: + for type in list(LockStatusType): + if code == type.code: + return type + class LockEventType(Enum): """ @@ -54,21 +120,18 @@ class LockEventType(Enum): TRASH_MODE = ('Trash mode', 2225) AUTO_CALIBRATED = ('Auto-calibrated', 2226) - def __init__(self, description: str, codes: Union[int, Sequence[int]]): + def __init__(self, description: str, code: int): self.description = description - if isinstance(codes, (list, Tuple)): - self.codes = codes - else: - self.codes = [codes] + self.code = code def describe(self): return self.description @classmethod def parse(cls, code: int) -> Optional["LockEventType"]: - for mode in list(LockEventType): - if code in mode.codes: - return mode + for type in list(LockEventType): + if code == type.code: + return type class LockEventSource(Enum): @@ -76,7 +139,7 @@ class LockEventSource(Enum): See: ford_lock_history_source """ - LOCAL = ('Local', 1) + LOCAL = ('App', 1) KEYPAD = ('Keypad', [2, 102]) FINGERPRINT = ('Fingerprint', 3) INSIDE_BUTTON = ('Inside button', 4) @@ -103,32 +166,176 @@ def parse(cls, code: int) -> Optional["LockEventSource"]: return mode +class LockVolumeLevel(Enum): + """ + See: ford_lock_setting_volume + """ + + OFF = ('Off', 0) + NORMAL = ('Normal', 50) + HIGH = ('High', 100) + + def __init__(self, description: str, code: int): + self.description = description + self.code = code + + def describe(self): + return self.description + + @classmethod + def parse(cls, code: int) -> Optional["LockVolumeLevel"]: + for level in list(LockVolumeLevel): + if code == level.code: + return level + + class LockLeftOpenTime(Enum): """ See: ford_open_alarm_time """ + IMMEDIATE = ('At once', 1) MIN_1 = ('1 min', 2) MIN_5 = ('5 min', 3) MIN_10 = ('10 min', 4) MIN_30 = ('30 min', 5) MIN_60 = ('60 min', 6) - def __init__(self, description: str, codes: Union[int, Sequence[int]]): + def __init__(self, description: str, code: int): self.description = description - if isinstance(codes, (list, Tuple)): - self.codes = codes - else: - self.codes = [codes] + self.code = code def describe(self): return self.description @classmethod def parse(cls, code: int) -> Optional["LockLeftOpenTime"]: - for mode in list(LockLeftOpenTime): - if code in mode.codes: - return mode + for item in list(LockLeftOpenTime): + if code == item.code: + return item + + +class LockKeyType(Enum): + """ + See: com.yunding.ydbleapi.bean.KeyInfo.type + """ + + BLUETOOTH = ('Bluetooth', 1) + ACCESS_CODE = ('Access Code', 2) + FINGERPRINT = ('Fingerprint', 3) + + def __init__(self, description: str, code: int): + self.description = description + self.code = code + + def describe(self): + return self.description + + @classmethod + def parse(cls, code: int) -> Optional["LockKeyType"]: + for type in list(LockKeyType): + if code == type.code: + return type + + +class LockKeyState(Enum): + """ + See: com.yunding.ydbleapi.bean.LockPasswordInfo.pwd_state + """ + + INIT = ('Init', 1) + IN_USE = ('In use', 2) + WILL_USE = ('Will use', 3) + OUT_OF_PERMISSION = ('Out of permission', 4) + FROZENED = ('Frozen', 5) + + def __init__(self, description: str, code: int): + self.description = description + self.code = code + + def describe(self): + return self.description + + @classmethod + def parse(cls, code: int) -> Optional["LockKeyState"]: + for state in list(LockKeyState): + if code == state.code: + return state + + +class LockKeyOperation(Enum): + """ + See: com.yunding.ydbleapi.bean.LockPasswordInfo.operation + """ + + ADD = ('Add', 1) + DELETE = ('Delete', 2) + UPDATE = ('Update', 3) + FROZEN = ('Freeze', 4) + UNFROZEN = ('Unfreeze', 5) + + def __init__(self, description: str, code: int): + self.description = description + self.code = code + + def describe(self): + return self.description + + @classmethod + def parse(cls, code: int) -> Optional["LockKeyOperation"]: + for operation in list(LockKeyOperation): + if code == operation.code: + return operation + + +class LockKeyOperationStage(Enum): + """ + See: com.yunding.ydbleapi.bean.LockPasswordInfo.operation_stage + """ + + GOING = ('Pending', 1) + INVALID = ('Failure', 2) + SUCCESS = ('Success', 3) + + def __init__(self, description: str, code: int): + self.description = description + self.code = code + + def describe(self): + return self.description + + @classmethod + def parse(cls, code: int) -> Optional["LockKeyOperationStage"]: + for stage in list(LockKeyOperationStage): + if code == stage.code: + return stage + + +class LockKeyPermissionType(Enum): + """ + See: com.yunding.ydbleapi.bean.YDPermission.status + """ + + ALWAYS = ('Always', 1) + DURATION = ('Temporary', 2) + ONCE = ('One-Time', 3) + RECURRING = ('Recurring', 4) + + def __init__(self, description: str, code: int): + self.description = description + self.code = code + + def describe(self): + return self.description + + @classmethod + def parse(cls, code: int) -> Optional["LockKeyPermissionType"]: + for type in list(LockKeyPermissionType): + if code == type.code: + return type + + def to_json(self): + return self.code class LockRecordDetail(JsonObject): @@ -152,6 +359,7 @@ def attributes(self) -> Set[str]: "source_name", "sourceid", "time", + "audio_played", } def __init__( @@ -168,6 +376,7 @@ def __init__( source_name: str = None, sourceid: int = None, time: datetime = None, + audio_played: int = None, **others: dict ): self.id = id if id else self._extract_attribute('id', others) @@ -187,8 +396,107 @@ def __init__( self.source_name = source_name if source_name else self._extract_attribute('source_name', others) self.sourceid = sourceid if sourceid else self._extract_attribute('sourceid', others) self.time = time if time else epoch_to_datetime(self._extract_attribute('time', others), ms=True) + self.audio_played = audio_played if audio_played else self._extract_attribute('audio_played', others) + show_unknown_key_warning(self, others) + + +class LockKeyPermission(JsonObject): + """ + A lock key permission. + + See: com.yunding.ydbleapi.bean.YDPermission + """ + + @property + def attributes(self) -> Set[str]: + return { + "type", + "begin", + "end", + } + + def __init__( + self, + *, + type: Optional[LockKeyPermissionType] = None, + begin: Optional[Union[int, datetime]] = None, + end: Optional[Union[int, datetime]] = None, + **others: dict + ): + self.type = type if type is not None else LockKeyPermissionType.parse(self._extract_attribute('status', others)) + if isinstance(begin, datetime): + self.begin = begin + else: + self.begin = epoch_to_datetime(begin if begin is not None else self._extract_attribute('begin', others), ms=True) + if isinstance(end, datetime): + self.end = end + else: + self.end = epoch_to_datetime(end if end is not None else self._extract_attribute('end', others), ms=True) + show_unknown_key_warning(self, others) + + def to_json(self): + to_return = {'status': self.type.to_json()} + if self.type == LockKeyPermissionType.DURATION or self.type == LockKeyPermissionType.ONCE: + if self.begin is not None: + to_return['begin'] = int(self.begin.replace(microsecond=0).timestamp()) + if self.end is not None: + to_return['end'] = int(self.end.replace(microsecond=0).timestamp()) + if self.type == LockKeyPermissionType.RECURRING: + to_return['begin'] = 0 + to_return['end'] = 0 + return to_return + + +class LockKeyPeriodicity(JsonObject): + """ + A lock key periodicity describing recurring access rules. + + See: com.yunding.ydbleapi.bean.PeriodicityInfo + """ + + @property + def attributes(self) -> Set[str]: + return { + "type", + "interval", + "begin", + "end", + "valid_days", + } + + def __init__( + self, + *, + begin: Union[str, int, time] = None, + end: Union[str, int, time] = None, + valid_days: Union[int, Sequence[int]] = None, + **others: dict + ): + self.type = 2 + self.interval = 1 + if isinstance(begin, time): + self.begin = begin + else: + self.begin = str_to_time(begin if begin is not None else self._extract_attribute('begin', others)) + if isinstance(end, time): + self.end = end + else: + self.end = str_to_time(end if end is not None else self._extract_attribute('end', others)) + if not isinstance(valid_days, (list, Tuple)): + valid_days = [valid_days] + self.valid_days = valid_days + show_unknown_key_warning(self, others) + def to_json(self): + return { + 'type': self.type, + 'interval': self.interval, + 'begin': self.begin.replace(second=0, microsecond=0).strftime('%H%M%S'), + 'end': self.end.replace(second=0, microsecond=0).strftime('%H%M%S'), + 'valid_days': self.valid_days, + } + class LockRecord(JsonObject): """ @@ -224,10 +532,8 @@ def __init__( self.type = type if type is not None else self._extract_attribute('eventid', others) if isinstance(details, LockRecordDetail): self.details = details - elif details is not None: - LockRecordDetail(**details) else: - self.details = LockRecordDetail(**self._extract_attribute('detail', others)) + self.details = LockRecordDetail(**(details if details is not None else self._extract_attribute('detail', others))) self.priority = priority if priority is not None else self._extract_attribute('priority', others) self.processed = processed if processed is not None else self._extract_attribute('processed', others) if isinstance(time, datetime): @@ -249,6 +555,175 @@ def type(self, value: Union[int, LockEventType]): self._type = value +class LockKey(JsonObject): + """ + A lock key. This can be either: + * a Bluetooth connection + * a password (hash of a numeric code) + * a fingerprint + BLE Actions: + * freeze (1) + * permission_state = 5 + * operation = 4 + * unfreeze (2) + * permission_state = 2 + * operation = 5 + * update (3) + * operation = 3 + * (set permission) + Password Actions: + * freeze (1) + * pwd_state = 5 + * operation = 4 + * operation_stage = 3 + * unfreeze (2) + * pwd_state = 2 + * operation = 5 + * operation_stage = 3 + * update (3) + * operation = 3 + * operation_stage = 3 + * (set permission) + Fingerprint Actions: + * freeze (1) + * fp_state = 5 + * operation = 4 + * operation_stage = 3 + * unfreeze (2) + * fp_state = 2 + * operation = 5 + * operation_stage = 3 + * update (3) + * operation = 3 + * operation_stage = 3 + * (set permission) + + See: com.yunding.ydbleapi.bean.KeyInfo + """ + + @property + def attributes(self) -> Set[str]: + return { + "id", + "type", + "time", + "name", + "description", + "is_default", + "notify", + "userid", + "username", + "permission", + "periodicity", + "operation", + "operation_stage", + "permission_state", # used with Bluetooth key + "pwd_state", # used with password key + } + + def __init__( + self, + *, + id: int = None, + type: LockKeyType = None, + time: datetime = None, + name: str = None, + description: str = None, + is_default: Union[int, bool] = None, + notify: Union[int, bool] = False, + userid: str = None, + username: str = None, + permission: Union[dict, LockKeyPermission] = None, + periodicity: Optional[Union[dict, LockKeyPeriodicity]] = None, + operation: Union[int, LockKeyOperation] = None, + operation_stage: Union[int, LockKeyOperationStage] = None, + permission_state: Optional[Union[int, LockKeyState]] = None, + pwd_state: Optional[Union[int, LockKeyState]] = None, + **others: dict + ): + self.id = id if id is not None else self._extract_attribute('id', others) + self.type = type + if isinstance(time, datetime): + self.time = time + else: + self.time = epoch_to_datetime(time if time is not None else self._extract_attribute('time', others), ms=True) + self.name = name if name is not None else self._extract_attribute('name', others) + self.description = description if description is not None else self._extract_attribute('description', others) + self.is_default = is_default if is_default is not None else self._extract_attribute('is_default', others) + self.notify = notify if notify is not None else self._extract_attribute('notify', others) + self.userid = userid if userid else self._extract_attribute('userid', others) + self.username = username if username else self._extract_attribute('username', others) + if isinstance(permission, LockKeyPermission): + self.permission = permission + else: + self.permission = LockKeyPermission(**permission) if permission is not None else LockKeyPermission(**self._extract_attribute('permission', others)) + if isinstance(periodicity, LockKeyPeriodicity): + self.periodicity = periodicity + else: + periodicity = periodicity if periodicity is not None else self._extract_attribute('period_info', others) + self.periodicity = LockKeyPeriodicity(**periodicity) if periodicity is not None else None + if not isinstance(operation, LockKeyOperation): + self.operation = LockKeyOperation.parse(operation if operation is not None else self._extract_attribute('operation', others)) + self.operation = operation + if not isinstance(operation_stage, LockKeyOperationStage): + self.operation_stage = LockKeyOperationStage.parse(operation_stage if operation_stage is not None else self._extract_attribute('operation_stage', others)) + self.operation_stage = operation_stage + if not isinstance(permission_state, LockKeyState): + self.permission_state = LockKeyState.parse(permission_state if permission_state is not None else self._extract_attribute('permission_state', others)) + self.permission_state = permission_state + if not isinstance(pwd_state, LockKeyState): + self.pwd_state = LockKeyState.parse(pwd_state if pwd_state is not None else self._extract_attribute('pwd_state', others)) + self.pwd_state = pwd_state + show_unknown_key_warning(self, others) + + @property + def is_default(self) -> bool: + return self._is_default + + @is_default.setter + def is_default(self, value: Union[int, bool]): + if isinstance(value, int): + value = True if value == 1 else False + self._is_default = value + + @property + def notify(self) -> bool: + return self._notify + + @notify.setter + def notify(self, value: Union[int, bool]): + if isinstance(value, int): + value = True if value == 1 else False + self._notify = value + + +class LockKeypad(VoltageMixin, Device): + + type = "LockKeypad" + + @property + def attributes(self) -> Set[str]: + return super().attributes.union({ + "uuid", + "power", + "is_enabled", + "onoff_time", + "power_refreshtime", + }) + + def __init__( + self, + is_enabled: bool = False, + **others: dict, + ): + super().__init__(type=self.type, **others) + self.uuid = super()._extract_attribute("uuid", others) + self.voltage = self._extract_property(prop_def=LockProps.voltage(), others=others) + self.is_enabled = is_enabled + self._is_online = self._extract_property(prop_def=LockProps.onoff_line(), others=others) + show_unknown_key_warning(self, others) + + class Lock(LockableMixin, ContactMixin, VoltageMixin, Device): type = "Lock" @@ -261,6 +736,15 @@ def attributes(self) -> Set[str]: "switch_state", "switch_state_ts", "parent", + "door_sensor", # Auto-Lock -> Door Position + "auto_lock_time", # Auto-Lock -> Auto-Lock/Timing + "trash_mode", # Auto-Lock -> Trash Mode + "auto_unlock", # Auto-Unlock -> Auto-Unlock + "keypad", + "ajar_alarm", # Alarm Settings -> Door Jam Alarm + "left_open_time", # Alarm Settings -> Left Open Alarm + "door_open_status", + "open_volume", "record_count", }) @@ -273,6 +757,14 @@ def parse_uuid(cls, mac: str) -> str: def __init__( self, parent: Optional[str] = None, + door_sensor: Union[int, bool] = None, + auto_lock_time: Union[int, bool, LockLeftOpenTime] = None, + trash_mode: Union[int, bool] = None, + auto_unlock: Union[int, bool] = None, + keypad: Optional[LockKeypad] = None, + ajar_alarm: Union[int, bool] = None, + left_open_time: Union[int, bool, LockLeftOpenTime] = None, + open_volume: Union[int, LockVolumeLevel] = None, record_count: Optional[int] = None, **others: dict, ): @@ -283,6 +775,48 @@ def __init__( self.open_close_state = self._extract_open_close_state(others) self.voltage = self._extract_property(prop_def=LockProps.voltage(), others=others) self._parent = parent if parent is not None else super()._extract_attribute("parent", others) + if ajar_alarm is None: + ajar_alarm = self._extract_attribute(name=LockProps.ajar_alarm().pid, others=others) + self.ajar_alarm = True if ajar_alarm is True or ajar_alarm == 1 else False + if isinstance(left_open_time, LockLeftOpenTime): + self.left_open_time = left_open_time + else: + if left_open_time is None: + left_open_time = self._extract_attribute(name=LockProps.left_open_time().pid, others=others) + if left_open_time is False or isinstance(left_open_time, int) and left_open_time == 0: + self.left_open_time = False + else: + self.left_open_time = LockLeftOpenTime.parse(left_open_time) + if door_sensor is None: + door_sensor = self._extract_attribute(name=LockProps.door_sensor().pid, others=others) + self.door_sensor = True if door_sensor is True or door_sensor == 1 else False + if isinstance(auto_lock_time, LockLeftOpenTime): + self.auto_lock_time = auto_lock_time + else: + if auto_lock_time is None: + auto_lock_time = self._extract_attribute(name=LockProps.auto_lock_time().pid, others=others) + if auto_lock_time is False or isinstance(auto_lock_time, int) and auto_lock_time == 0: + self.auto_lock_time = False + else: + self.auto_lock_time = LockLeftOpenTime.parse(auto_lock_time) + if trash_mode is None: + trash_mode = self._extract_attribute(name=LockProps.trash_mode().pid, others=others) + self.trash_mode = True if trash_mode is True or trash_mode == 1 else False + if auto_unlock is None: + auto_unlock = self._extract_attribute(name=LockProps.auto_unlock().pid, others=others) + self.auto_unlock = True if auto_unlock is True or auto_unlock == 1 else False + if keypad is None: + keypad = super()._extract_attribute("keypad", others) + if keypad is not None: + keypad_enable_status = super()._extract_attribute("keypad_enable_status", others) + keypad = LockKeypad(**keypad, is_enabled=True if keypad_enable_status == 1 else False) + self.keypad = keypad + if isinstance(open_volume, LockVolumeLevel): + self.open_volume = open_volume + else: + if open_volume is None: + open_volume = self._extract_attribute(name=LockProps.open_volume().pid, others=others) + self.open_volume = LockVolumeLevel.parse(open_volume) self._record_count = record_count if record_count is not None else super()._extract_attribute("record_count", others) show_unknown_key_warning(self, others) @@ -292,7 +826,7 @@ def _extract_lock_state(self, others: Union[dict, Sequence[dict]]) -> DeviceProp prop_def = LockProps.locker_lock_state() value = super()._extract_property(prop_def=prop_def, others=others["device_params"]["locker_status"]) ts = super()._extract_attribute(name=prop_def.pid + "_refreshtime", others=others["device_params"]["locker_status"]) - self.logger.debug(f"returning new DeviceProp with {value}") + self.logger.debug(f"returning new DeviceProp with value {value.value}") return DeviceProp(definition=prop_def, ts=ts, value=value.value) # if switch_state == 1, device is UNlocked so we have to flip the bit prop = super()._extract_property(prop_def=LockProps.lock_state(), others=others) @@ -319,6 +853,16 @@ def parent(self) -> str: def record_count(self) -> int: return self._record_count + @property + def is_locked(self) -> bool: + # this is janky...a lock needs to store a lock status + if super().lock_state is None: + return False + if isinstance(super().lock_state.definition.type, bool): + return super().lock_state + # return True if super().lock_state.value == 1 else True + return super().lock_state.value == LockStatusType.LOCKED.code + class LockGateway(AbstractWirelessNetworkedDevice): diff --git a/wyze_sdk/service/ford_service.py b/wyze_sdk/service/ford_service.py index 27741da..cea9d76 100644 --- a/wyze_sdk/service/ford_service.py +++ b/wyze_sdk/service/ford_service.py @@ -1,4 +1,5 @@ from __future__ import annotations +import json import logging import urllib @@ -7,11 +8,18 @@ import wyze_sdk.errors as e from wyze_sdk.models import datetime_to_epoch +from wyze_sdk.models.devices.locks import LockKeyPeriodicity, LockKeyPermission from wyze_sdk.signature import RequestVerifier from .base import BaseServiceClient, WyzeResponse +def default(obj): + if hasattr(obj, 'to_json'): + return obj.to_json() + raise TypeError(f'Object of type {obj.__class__.__name__} is not JSON serializable') + + class FordResponse(WyzeResponse): def validate(self) -> WyzeResponse: @@ -47,6 +55,7 @@ class FordServiceClient(BaseServiceClient): WYZE_API_URL = "https://yd-saas-toc.wyzecam.com" WYZE_FORD_APP_KEY = "275965684684dbdaf29a0ed9" WYZE_FORD_APP_SECRET = "4deekof1ba311c5c33a9cb8e12787e8c" + WYZE_FORD_IV_HEX = "0123456789ABCDEF" def __init__( self, @@ -98,7 +107,9 @@ def api_call( json = {} # this must be done here so that it will be included in the signing json.update({ - "access_token": self.token, + # suddenly, the app started using `accessToken` instead of `access_token` + # in POST requests :| + "accessToken": self.token, "key": self.WYZE_FORD_APP_KEY, "timestamp": str(nonce), }) @@ -133,13 +144,19 @@ def get_user_device(self, limit: int = 25, offset: int = 0, **kwargs) -> FordRes kwargs.update({'limit': str(limit), "offset": str(offset), "detail": "1"}) return self.api_call('/openapi/v1/device', params=kwargs) - def get_lock_info(self, *, uuid: str, **kwargs) -> FordResponse: + def get_lock_info(self, *, uuid: str, with_keypad: bool = True, **kwargs) -> FordResponse: """ See: com.yunding.ford.manager.NetLockManager.getLockInfo """ kwargs.update({'uuid': uuid}) + if with_keypad: + kwargs.update({'with_keypad': 1}) return self.api_call('/openapi/lock/v1/info', params=kwargs) + def get_keypad_info(self, *, uuid: str, **kwargs) -> FordResponse: + kwargs.update({'uuid': uuid}) + return self.api_call('/openapi/keypad/v1/info', params=kwargs) + def get_gateway_info(self, *, uuid: str, **kwargs) -> FordResponse: kwargs.update({'uuid': uuid}) return self.api_call('/openapi/gateway/v1/info', params=kwargs) @@ -159,7 +176,7 @@ def get_family_record_count(self, *, uuid: str, begin: datetime, end: Optional[d kwargs.update({'end': datetime_to_epoch(end)}) return self.api_call('/openapi/v1/safety/count', params=kwargs) - def get_family_record(self, *, uuid: str, begin: datetime, end: Optional[datetime] = None, offset: int = 0, limit: int = 20, **kwargs) -> FordResponse: + def get_family_records(self, *, uuid: str, begin: datetime, end: Optional[datetime] = None, offset: int = 0, limit: int = 20, **kwargs) -> FordResponse: """ Gets a reverse chronological list of lock event records. `begin` is the earliest time. @@ -176,3 +193,43 @@ def remote_control_lock(self, *, uuid: str, action: str, **kwargs) -> FordRespon """ kwargs.update({'uuid': uuid, 'action': action}) return self.api_call('/openapi/lock/v1/control', http_verb="POST", json=kwargs) + + def get_passwords(self, *, uuid: str, **kwargs) -> FordResponse: + kwargs.update({'uuid': uuid}) + return self.api_call('/openapi/lock/v1/pwd', params=kwargs) + + def add_password(self, *, uuid: str, password: str = None, name: str = None, permission: LockKeyPermission, periodicity: Optional[LockKeyPeriodicity] = None, userid: str, **kwargs) -> FordResponse: + kwargs.update({ + 'uuid': uuid, + 'userid': userid, + }) + kwargs.update({'permission': json.dumps(permission, default=default)}) + if periodicity is not None: + kwargs.update({'period_info': json.dumps(periodicity, default=default)}) + if password is not None: + kwargs.update({'password': password}) + if name is not None: + kwargs.update({'name': name}) + return self.api_call('/openapi/lock/v1/pwd/operations/add', http_verb="POST", json=kwargs) + + def update_password(self, *, uuid: str, password_id: str, password: Optional[str] = None, name: str = None, permission: Optional[LockKeyPermission] = None, periodicity: Optional[LockKeyPeriodicity] = None, **kwargs) -> FordResponse: + kwargs.update({ + 'uuid': uuid, + 'passwordid': password_id, + }) + if permission is not None: + kwargs.update({'permission': json.dumps(permission, default=default)}) + if periodicity is not None: + kwargs.update({'period_info': json.dumps(periodicity, default=default)}) + if password is not None: + kwargs.update({'password': password}) + if name is not None: + kwargs.update({'name': name}) + return self.api_call('/openapi/lock/v1/pwd/operations/update', http_verb="POST", json=kwargs) + + def delete_password(self, *, uuid: str, password_id: str, **kwargs) -> FordResponse: + kwargs.update({ + 'uuid': uuid, + 'passwordid': password_id, + }) + return self.api_call('/openapi/lock/v1/pwd/operations/delete', http_verb="POST", json=kwargs) diff --git a/wyze_sdk/signature/__init__.py b/wyze_sdk/signature/__init__.py index c42da29..4bbaf66 100644 --- a/wyze_sdk/signature/__init__.py +++ b/wyze_sdk/signature/__init__.py @@ -1,5 +1,7 @@ import hashlib import hmac +from Cryptodome.Cipher import AES +from Cryptodome.Util.Padding import pad, unpad from time import time from typing import Optional, Union @@ -64,3 +66,40 @@ def generate_dynamic_signature( request_hash = hmac.new(encoded_secret, format_req, hashlib.md5).hexdigest() calculated_signature = f"{request_hash}" return calculated_signature + + +class MD5Hasher: + + def hash(self, data: Union[str, bytes] = "") -> bytes: + if isinstance(data, str): + data = data.encode() + return hashlib.md5(data).digest() + + def hex(self, data: Union[str, bytes] = "") -> str: + if isinstance(data, str): + data = data.encode() + return hashlib.md5(data).hexdigest() + + +class CBCEncryptor: + + def __init__(self, iv: Union[str, bytes]): + if isinstance(iv, str): + iv = iv.encode() + self.iv = iv + + def encrypt(self, key: Union[str, bytes], data: Union[str, bytes]) -> bytes: + if isinstance(key, str): + key = key.encode() + if isinstance(data, str): + data = data.encode() + cipher = AES.new(key, AES.MODE_CBC, self.iv) + return cipher.encrypt(pad(data, AES.block_size)) + + def decrypt(self, key: Union[str, bytes], data: Union[str, bytes]) -> bytes: + if isinstance(key, str): + key = key.encode() + if isinstance(data, str): + data = bytes.fromhex(data) + cipher = AES.new(key, AES.MODE_CBC, self.iv) + return unpad(cipher.decrypt(data), AES.block_size)