diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index ae95bf4..986e6fe 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,16 +1,17 @@ -d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example -a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore +41dfdc6d099b54f87d4dd51f696122b3c88d3bf3ddf420bea8f4bc621ba920b0 ./build.rs 6877eacd3931f7922f3f379d46e8cd9de7a609a65060ce2285c09b9c95f230aa ./Cargo.lock ebe64386eb553c298994df40f73a56b4980eedcd88a84cb310134070c1753eb3 ./Cargo.toml -19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -92633f07b839dcdeafa96cb3ffdaaa600f65387a9113ed595987e68982fed0d6 ./README.md -41dfdc6d099b54f87d4dd51f696122b3c88d3bf3ddf420bea8f4bc621ba920b0 ./build.rs +537cb63c8f8420cc57656333d607c2ae6c908082253c831de8945c4c2dee7e4c ./data/gree-controller.db +83ac2f5e7ea116c28873a295d19e27a6e50449ac44d66b3650e559fc17ad0d81 ./data/gree-controller.db-shm +45a4e65ecc096d62c79a7eab907776f65c5bda4bc2248ba3348726cb2bb89f37 ./data/gree-controller.db-wal 3596e68d53d614ff784cdaa7a26eabd651cf02909da770bc863518f1218299a0 ./docs/API.md 1b522e1b61643733accf0a45b8b862cfb96e1468343c9819600e263fe67a2b96 ./docs/CODE_AUDIT_0.12.0.md 2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md 3ba861e16d523f962574334ed74d53d6be2669dcb05851578d7a5434e8856f4e ./docs/openapi.json -a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md -f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py +d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env +d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example +a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore +8d48f0962dca59fb590f37f112791cc876a81efea815691dca9feb61d16ca578 ./gree-controller.zip 6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py 71c8058f85fd9ce9f063b7a84dcf2956f23cdbbd02bd4bd99d2fbb9d177c72ad ./home-assistant/custom_components/gree_controller/climate.py 5e4aef2143e81bedb5a15dd4be5c71b64a3ab448ec6d33a20851edd098e5f529 ./home-assistant/custom_components/gree_controller/config_flow.py @@ -18,6 +19,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./home-assista ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assistant/custom_components/gree_controller/coordinator.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png +f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png 9538b4a08e6c76dbd5850f3720f582253fd8e4542871891a2cf5552759fb334a ./home-assistant/custom_components/gree_controller/manifest.json 38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py @@ -27,9 +29,11 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assista 6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./home-assistant/custom_components/gree_controller/translations/en.json 13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json 4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json -253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md +a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md 2444b048c40ccb3ebbc161663ccff8409f8d6763858943075c033ea8f832fdef ./lang/en.json 1b3eee0b9ece41012535bfbe07ddd7c228aa161d1aa0bc77850a39177a062928 ./lang/pl.json +253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md +19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py 1e7b1bcf4194abfb8bad0d601268af0547d58541731e98b74a3500d7629d0be0 ./mock/index.html 007c78e0cb404778985980b23bb5f8bbc7a8f04c80015204e24c321970e3b15e ./mock/mock.js @@ -72,9 +76,9 @@ fffa18bd989a0dcc1c5458975522517d40af4af40cc1ca30640e79d3db711053 ./presets/unoc ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/weekend_comfort.json fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json +92633f07b839dcdeafa96cb3ffdaaa600f65387a9113ed595987e68982fed0d6 ./README.md 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh -e8fc6cb061f27fb81b069de1f08cfa2d3eb26557ec2221abda64b621b8092750 ./scripts/FILE_MANIFEST.sha256 -bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md +966c7b81c07c07d9073f797388f695164095a520753910eb26c5daa64caaae02 ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -82,12 +86,12 @@ bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/READ e4849261fd9ed1f01df96c0637c439c0c4eff8fa317b2918026167bba343af79 ./scripts/install-lxc.sh bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/install.sh e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh +bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh 7e8f558b284362f55f50bd7223a89754a2a4960e746a27035cc61180f4a2da6f ./scripts/smoke.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh 0790b7b388467d9e75b621188cca473e22d73e01bcfa74e92fa23f92b0695855 ./scripts/verify_flow_logic.py -11a2bca9c4f349fb5fb5090e02607f26bddf692a739fea0384779820935fcea7 ./src/api.rs 825370f409124a719043200742c01c3441bc65f483108106140d13fe7bbcba91 ./src/api/assets.rs 6c34b1294d76b0eba4c56bfd0bf69bdd502de0e40838104f6ddf2db4e922683d ./src/api/auth.rs 63cb241c4536abed402f2a0ad37e7f299343df264eab6a33bb5354c15421e045 ./src/api/automations.rs @@ -102,13 +106,14 @@ d397e06b2034490b1238a8cf4663661a4e68c34d18a3037169e7332bd9e79cf2 ./src/api/hous 88e04a59bfedd5e6c54e6a2938b1031964be3a1505db2ca67d6290aa042cbe3b ./src/api/integrations.rs bb2a746ecdcc2da5fe54e18b455c7bd19453486dd5c008e71951a2c81d0e7d64 ./src/api/middleware.rs 45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs +50cfd47e44e22cc802f22f97267c157fa5a6d19b51be921a6ca6b1ea7c9799d1 ./src/api/public_settings.rs +11a2bca9c4f349fb5fb5090e02607f26bddf692a739fea0384779820935fcea7 ./src/api.rs a1d1a4ac071493b052e6bf20b03be67cf75726de95649e9a73aeba8d22395a15 ./src/api/schedules.rs c1275fb8fcd2b2ba24e9c6b1234f25ce6865517c792e97ad46e14ec8bb16ed83 ./src/api/settings.rs 6f53b89c96a99a85c3badd6af169fda9c978630516279b57b756b5468caef7d7 ./src/api/system.rs 681cccb8d09f4ad9c2e6467a4dbe2f9d991125775a5d2bfc9043daf940fecd29 ./src/api/websocket.rs 525c4e546a611d26006f6414fc88ab4c6773171ed83bd1ca85644945139b5b06 ./src/api/zones.rs 3cec9426989034a6a5074aead64e8c0239ae942a96a04910c57d1593129c5bb8 ./src/config.rs -c8b8b4a04c94b9d91f5931057d69ad38344730bffbf68e2f6541b59013441b57 ./src/db.rs 24580b279cfcd5388ba584eaba2c9bb729b8aae81e77b781c23fb804c5b5fa5d ./src/db/climate.rs 6998f88a6a030ef9a1fa3a46466f6dfb79c6df086411d94c82ecd69fa1690e71 ./src/db/configuration.rs 56b0efe84dbf6bd3ee2397b099220a68966311c187f4d5b53007a1693a5ef327 ./src/db/core_devices.rs @@ -116,10 +121,10 @@ d8a2b323e864f2ec38ee15e3023be37a6aa05387f1ddbeedafe1f51556ad8c7d ./src/db/devic f3bd91d0bdb7699f319b9341d928d3cc3b25f759653fcd2095a5c5f695d08fac ./src/db/events_tokens.rs 03d689fb03c4f61629fff8404b6cea7101ba3a5358247b16ad714e0be93e9150 ./src/db/flows.rs 59912e70b688daf5c4d22679d643ed3b724c262d62466aeaa9ef74db87808178 ./src/db/ha_history.rs +c8b8b4a04c94b9d91f5931057d69ad38344730bffbf68e2f6541b59013441b57 ./src/db.rs 7e2fbce70f40aef360534ba38c6700163db77cb7288b3bf58728fce72a126435 ./src/db/schedules_automations.rs 35cbb521a8ec456756496b8aee71f9dfebad528a2cf2653ba8b48f6035be0c01 ./src/db/tests.rs a384ea4042d1d11692b5f8693df1f54850dcc4e80726780b44b2b096bf667b78 ./src/db/zone_history.rs -fbebb9815bdabbad9ff4e979f8ea9dc3dda9c77790f30a64fa0b392fb1944d00 ./src/engine.rs ec6034df6667b3ac6ec39851443048a56e87fa861f5846950a5e79fa89ee5a30 ./src/engine/automations.rs 77931af524e9402fc826050b5fcec422d01b02b625c8ce7f2004f1f6520c9b4d ./src/engine/commands.rs 750c5c219c7c8a72f51eb6dfc679d21c5821c122861d9cfbaba57485b66a33a4 ./src/engine/control_plan.rs @@ -129,6 +134,7 @@ c1410aae910b8239e48857df1f0484f08261ad6cf385b596f7c538e920907437 ./src/engine/g 4972c199274647bff47122c367e51bae8dab24d7ec4d652288e227a5a64c2310 ./src/engine/local_thermostat.rs abf9eb5c2444f540f1601682d4c7f3e86f6742690f6af621999406cd81f6a2e4 ./src/engine/ownership.rs 9c37adf514c96840fd6a5c715c579350fa86d6dcc0655a2aa97d1fd7c7e04299 ./src/engine/polling.rs +fbebb9815bdabbad9ff4e979f8ea9dc3dda9c77790f30a64fa0b392fb1944d00 ./src/engine.rs 3d48608a065aa52a1fd8b9c59356a74d8216ecee3d47f8ff8ef7d3d47170c617 ./src/engine/runtime.rs ffa72b93502eece187a96adb4ec94aec6048d4a6dc7af66e7c7af0931318787f ./src/engine/schedules.rs 7742046c0067c7cd5a74e2b81f8a8bdef9b76fcacf9b4af0b7eede1b050b9036 ./src/engine/targets.rs @@ -139,12 +145,11 @@ a9188b588b2617ce1547a45d46141f7e5f1bb0027a48675dd86b274f807d0875 ./src/engine/t 7db75c2d40df2af3759d0bd242133612d4ff768c8f26db0009853d3dcad5ade6 ./src/engine/zone_control.rs 4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs 9eef7820fcf0c98b08fb1bf5e0a4a244b1dbdfa700d8b930d3ff40379babdada ./src/home_assistant.rs -6f9ef85cd53ba030b477bc3c186f0f7e762674124210eabcb0686f2806eaada1 ./src/influxdb.rs feb50ecd60b9a9fe9de1f7d8e1e26caab5435d59347468f6266a318f431bc3d3 ./src/influxdb/codec.rs 553b44ec0321e62480059bc2451fd34ec7aabe9fc87fc717daba0c4b55679d53 ./src/influxdb/query.rs +6f9ef85cd53ba030b477bc3c186f0f7e762674124210eabcb0686f2806eaada1 ./src/influxdb.rs 5e4d94f8a010df05bae9acab72c518080d2dba94d054df95b87e492305357222 ./src/influxdb/write.rs c3ff7232a2fa1d29428872a10cfd72ee60a3a1faaa662eff6c2d8c7dffc1ed3a ./src/main.rs -1030e49498f8d595cf8fdee60651e7fec79d78061eb8561f9b29948b28c2f95f ./src/models.rs 67f189c195426c39669178dd92d8602c519806acca7d325665076720f9fee8a9 ./src/models/automation.rs 377fc9697a70a03ad35fc3701adf4dbe8687760dc90de9f5c532a4f3b612c762 ./src/models/control_plan.rs 8228b9b6d9e8b1d13de29ce88500beacd88c8877893b7a3e41a4bd1e237ca36c ./src/models/defaults.rs @@ -152,13 +157,13 @@ c3ff7232a2fa1d29428872a10cfd72ee60a3a1faaa662eff6c2d8c7dffc1ed3a ./src/main.rs aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs cfbaa853a5185c4be572588577b313a2d618837f7c07a3860d0e8d2cca04ee60 ./src/models/history.rs 552dfc1717ea88f3a23b1fb7705f1dbb0df7792ca9a6ae7ad7bd275eacef27af ./src/models/integrations.rs +1030e49498f8d595cf8fdee60651e7fec79d78061eb8561f9b29948b28c2f95f ./src/models.rs 8294749dd3af524925309c51ad641ffd32ff1fd9c164cfa71660aa88c2d13d6e ./src/models/runtime.rs 887eb80a58998e90023e952c1fde67435eb843493387ae64536613b74c084534 ./src/models/settings_api.rs e77f8cdcb5fc0e51cdbf2527a69585a2f6f9089e159d1e45ec4a48735ab0e304 ./src/models/temporary_thermostat.rs 5b5c80e25ae2aad1b511bd3f0a82b8fbae2440abf8c656ecb90814587d0be137 ./src/models/zone.rs 9c735d7c475f21f2f388c21be784376f7184f0bf18af1886544ef51585a3b290 ./src/notifications.rs 7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs -f1f765331469f4219a551414833c19b6de413114c22fdd0468e1f417659553c9 ./src/protocol/gree.rs c935e441933ba8709432cc3ac33ed868043a89188d0f48ea6fbfe34b1f8936a3 ./src/protocol/gree/binding.rs 563ded1758b135eb523f1fa0b1bf55fe5430ef55935b301fd6d79cbb980e1820 ./src/protocol/gree/commands.rs 5d4328199e2285016fd1941e909e5c09f7cdf9fd21ab6538c1bf8bc2f4af3ba3 ./src/protocol/gree/core.rs @@ -166,14 +171,15 @@ c935e441933ba8709432cc3ac33ed868043a89188d0f48ea6fbfe34b1f8936a3 ./src/protocol 97284ad37767625cfc2b82a3de5c40f6eecddabe185bd0bcea7cf9b165e017f0 ./src/protocol/gree/merge.rs a96061fbbcadac6c2df2e9708a48dcf819b951eb147f32c7d0d285ac5ae2491b ./src/protocol/gree/network.rs 7763855207c23357cb67256ad9a47f89f8313280bc1f07ccac8ae21967a5ec7d ./src/protocol/gree/polling.rs +f1f765331469f4219a551414833c19b6de413114c22fdd0468e1f417659553c9 ./src/protocol/gree.rs 35fc903404045129e3046b7997e941b88f98ceb42485cccc872c2e4aa530c75b ./src/protocol/gree/tests.rs 55c2d615e76c9e4b08c4067ea734e81d81915144e637f3b51c19bea4fc879137 ./src/protocol/gree/transport.rs a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol/mod.rs -830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs 7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs 3fb50f57d30ac23ef122c45dae4bd96c7cb7184df02d99aef00d9890b66479e2 ./src/queries/entities.rs 2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs c91e4b8b250b30cbe76f313b70c16244e9003bce92e02c82283a1831f91974c5 ./src/queries/maintenance.rs +830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs 3a49bad74a2f047893e726b5e00a6e833c228ad268bdf50cd60f786ba164ea54 ./src/queries/schema.rs f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs 6ee23fcaa62231d15006db078d4f763d1b67d2365fcc2d736d8a7da8df5ea382 ./src/state.rs @@ -181,7 +187,6 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg f78622afea899a5e904910217eceeeee306d5fc8094bdccc049bd84752780720 ./web/index.html -9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md 8fb20fdba11df68feb0b096bb5b4c8f936db974aed5d345a0d01455054f31f95 ./web/js/bootstrap.js dee7c4292889ddd721c220e86fae909f0d662b585e6d9c5d9e2f8fa85d9ed2ad ./web/js/charts.js f75449450a179f5853b8b08c69dec93205985e0dcbc806d355fd6060237761f6 ./web/js/core.js @@ -193,10 +198,11 @@ f06557c6d338259059b990f8f79c91d68ac8ec1f245c302468f08929b5506901 ./web/js/forms dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js 7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js a0959f9caf84f1873e3549261de8b1a069f4df8326b17bfe1f8c3de120299875 ./web/js/navigation.js +9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md 2bc9cfa306cdeafff39a7bfc8c5744568e0130d88432351cba8b062178aefce3 ./web/js/realtime.js c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js -bdeb55546a1dff5e5bdfde4b114d6330f025df1b62e528bf18a93ad6dd4c3e83 ./web/js/settings-ui.js 3c4a96bbc73cdfaf2f76798201f88ef43776875dce42f2b75d6a1808cef496d9 ./web/js/settings.js +bdeb55546a1dff5e5bdfde4b114d6330f025df1b62e528bf18a93ad6dd4c3e83 ./web/js/settings-ui.js 6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest c5d515b987d96ebfbc7593e9bac9aff64c8d54420f23cc8df5e2c84b14a69852 ./web/styles.css ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js diff --git a/scripts/FILE_MANIFEST.sha256 b/scripts/FILE_MANIFEST.sha256 deleted file mode 100644 index 18a3024..0000000 --- a/scripts/FILE_MANIFEST.sha256 +++ /dev/null @@ -1,13 +0,0 @@ -bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./README.md -5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./common.sh -6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./configure-gree-network.sh -1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./dev.sh -14076104c042fba1284ebb07531a6c3ff972df1f9f5b18f70da18ab774efed27 ./generate_ha_migration.py -e4849261fd9ed1f01df96c0637c439c0c4eff8fa317b2918026167bba343af79 ./install-lxc.sh -bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./install.sh -e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./network-debug.sh -01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh -81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./service.sh -7e8f558b284362f55f50bd7223a89754a2a4960e746a27035cc61180f4a2da6f ./smoke.sh -b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./update.sh -0790b7b388467d9e75b621188cca473e22d73e01bcfa74e92fa23f92b0695855 ./verify_flow_logic.py diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py new file mode 100644 index 0000000..24d5f40 --- /dev/null +++ b/scripts/api_dev_test.py @@ -0,0 +1,611 @@ +#!/usr/bin/env python3 +"""Developer smoke/integration tests for GREE Controller API 0.12.0. + +Default mode is read-only and safe to run against a real controller. +Use --settings-write to additionally round-trip all split settings resources and +exercise selected validation failures. This writes the same settings back and +therefore creates settings/events and can trigger normal settings side effects. + +No third-party dependencies are required. +""" +from __future__ import annotations + +import argparse +import copy +import json +import os +import ssl +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Callable, Iterable, Optional + +DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787") +DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "") +DEFAULT_EXPECTED_VERSION = "0.12.0" + +SETTINGS_PATHS: dict[str, set[str]] = { + "/api/settings/application": {"simulator_enabled"}, + "/api/settings/gree": { + "controller_id", + "poll_interval_seconds", + "zone_interval_seconds", + "discovery_timeout_ms", + "discovery_broadcast", + "suppress_device_beep", + "compressor_protection_enabled", + "compressor_protection_seconds", + }, + "/api/settings/history": {"retention_days", "compaction_enabled", "event_retention_days"}, + "/api/settings/influxdb": { + "enabled", + "version", + "url", + "database", + "username", + "password_configured", + "org", + "bucket", + "token_configured", + "history_threshold_days", + }, + "/api/settings/notifications": { + "enabled", + "mode", + "provider", + "pushover_configured", + "slack_configured", + "discord_configured", + "cooldown_seconds", + "communication_failure_threshold", + "target_timeout_minutes", + "alert_types", + }, + "/api/settings/night": { + "enabled", "start_time", "end_time", "max_fan_speed", "force_quiet", "use_native_sleep" + }, + "/api/settings/home-assistant": { + "url", + "token_configured", + "default_entity_id", + "outdoor_entity_id", + "sensor_stale_after_seconds", + "allow_invalid_tls", + "sensor_aliases", + "flow_inputs", + "outdoor_assist_enabled", + }, + "/api/settings/debug": {"overlay_enabled", "gree_frames"}, +} + +REMOVED_0120_PATHS = [ + "/api/settings", + "/api/debug", + "/api/events/retention", + "/api/settings/export", + "/api/settings/import", +] + +SAFE_GET_PATHS = [ + "/api/bootstrap", + "/api/system/info", + "/api/devices", + "/api/zones", + "/api/groups", + "/api/schedules", + "/api/automations", + "/api/flows", + "/api/readings", + "/api/history", + "/api/control-plan", + "/api/events", + "/api/access-tokens", + "/api/configuration/export", +] + +# These are administrator-authenticated restricted HA reads. They are safe but +# can legitimately fail if the server is configured to forbid this surface. +HA_SAFE_GET_PATHS = [ + "/api/integrations/home-assistant/devices", + "/api/integrations/home-assistant/control-plan", + "/api/integrations/home-assistant/groups", +] + +DETAIL_COLLECTIONS = [ + ("/api/devices", "/api/devices/{id}"), + ("/api/zones", "/api/zones/{id}"), + ("/api/groups", "/api/groups/{id}"), + ("/api/schedules", "/api/schedules/{id}"), + ("/api/automations", "/api/automations/{id}"), + ("/api/flows", "/api/flows/{id}"), +] + +EXPECTED_OPENAPI_PATHS = set(SETTINGS_PATHS) | { + "/api/configuration/export", + "/api/configuration/import", +} + + +class TestFailure(AssertionError): + pass + + +@dataclass +class HttpResponse: + status: int + headers: dict[str, str] + body: bytes + elapsed_ms: float + + def text(self) -> str: + return self.body.decode("utf-8", errors="replace") + + def json(self) -> Any: + try: + return json.loads(self.text()) + except json.JSONDecodeError as exc: + raise TestFailure(f"response is not valid JSON: {exc}; body={self.text()[:300]!r}") from exc + + +class ApiClient: + def __init__(self, base_url: str, token: str, timeout: float, insecure: bool) -> None: + self.base_url = base_url.rstrip("/") + self.token = token.strip() + self.timeout = timeout + self.ssl_context: Optional[ssl.SSLContext] = None + if insecure: + self.ssl_context = ssl._create_unverified_context() # noqa: SLF001 - explicit developer option + + def _url(self, path: str) -> str: + if not path.startswith("/"): + path = "/" + path + return self.base_url + path + + def request( + self, + method: str, + path: str, + payload: Any = None, + *, + auth: bool = True, + headers: Optional[dict[str, str]] = None, + ) -> HttpResponse: + req_headers = {"Accept": "application/json"} + if auth and self.token: + req_headers["Authorization"] = f"Bearer {self.token}" + if headers: + req_headers.update(headers) + + data: Optional[bytes] = None + if payload is not None: + data = json.dumps(payload, ensure_ascii=False).encode("utf-8") + req_headers["Content-Type"] = "application/json" + + req = urllib.request.Request( + self._url(path), + data=data, + headers=req_headers, + method=method.upper(), + ) + started = time.perf_counter() + try: + with urllib.request.urlopen(req, timeout=self.timeout, context=self.ssl_context) as resp: + body = resp.read() + status = resp.getcode() + response_headers = {k.lower(): v for k, v in resp.headers.items()} + except urllib.error.HTTPError as exc: + body = exc.read() + status = exc.code + response_headers = {k.lower(): v for k, v in exc.headers.items()} + except urllib.error.URLError as exc: + raise TestFailure(f"cannot connect to {self._url(path)}: {exc.reason}") from exc + elapsed_ms = (time.perf_counter() - started) * 1000.0 + return HttpResponse(status, response_headers, body, elapsed_ms) + + def get(self, path: str, *, auth: bool = True) -> HttpResponse: + return self.request("GET", path, auth=auth) + + def put(self, path: str, payload: Any) -> HttpResponse: + return self.request("PUT", path, payload) + + +@dataclass +class Result: + name: str + status: str + detail: str = "" + elapsed_ms: float = 0.0 + + +@dataclass +class Runner: + client: ApiClient + verbose: bool = False + results: list[Result] = field(default_factory=list) + + def run(self, name: str, fn: Callable[[], Optional[str]]) -> None: + started = time.perf_counter() + try: + detail = fn() or "" + status = "PASS" + except SkipTest as exc: + status = "SKIP" + detail = str(exc) + except Exception as exc: # deliberate: one failed test must not stop the suite + status = "FAIL" + detail = str(exc) + elapsed_ms = (time.perf_counter() - started) * 1000.0 + self.results.append(Result(name, status, detail, elapsed_ms)) + marker = {"PASS": "+", "FAIL": "!", "SKIP": "-"}[status] + suffix = f" - {detail}" if detail and (self.verbose or status != "PASS") else "" + print(f"[{marker}] {status:<4} {name} ({elapsed_ms:.0f} ms){suffix}") + + def summary(self) -> int: + counts = {name: sum(r.status == name for r in self.results) for name in ("PASS", "FAIL", "SKIP")} + print("\nSummary: " + ", ".join(f"{k}={v}" for k, v in counts.items())) + if counts["FAIL"]: + print("\nFailures:") + for result in self.results: + if result.status == "FAIL": + print(f" - {result.name}: {result.detail}") + return 1 if counts["FAIL"] else 0 + + +class SkipTest(Exception): + pass + + +def assert_status(resp: HttpResponse, *expected: int) -> None: + if resp.status not in expected: + body = resp.text().strip().replace("\n", " ")[:500] + raise TestFailure(f"HTTP {resp.status}, expected {expected}; body={body!r}") + + +def assert_json_object(resp: HttpResponse) -> dict[str, Any]: + value = resp.json() + if not isinstance(value, dict): + raise TestFailure(f"expected JSON object, got {type(value).__name__}") + return value + + +def assert_json_collection(resp: HttpResponse) -> Any: + value = resp.json() + if not isinstance(value, (list, dict)): + raise TestFailure(f"expected JSON collection/object, got {type(value).__name__}") + return value + + +def first_item(value: Any) -> Optional[dict[str, Any]]: + if isinstance(value, list): + return value[0] if value and isinstance(value[0], dict) else None + if isinstance(value, dict): + # Handle APIs that wrap arrays in a named property. + for candidate in value.values(): + if isinstance(candidate, list) and candidate and isinstance(candidate[0], dict): + return candidate[0] + return None + + +def convert_settings_view_to_update(path: str, view: dict[str, Any]) -> dict[str, Any]: + """Build a PUT payload from a GET view without modifying stored secrets.""" + payload = copy.deepcopy(view) + if path == "/api/settings/influxdb": + payload.pop("password_configured", None) + payload.pop("token_configured", None) + # Omitted Option fields preserve existing secrets. + elif path == "/api/settings/notifications": + payload.pop("pushover_configured", None) + payload.pop("slack_configured", None) + payload.pop("discord_configured", None) + elif path == "/api/settings/home-assistant": + payload.pop("token_configured", None) + return payload + + +def comparable_settings_view(path: str, view: dict[str, Any]) -> dict[str, Any]: + """Normalize a settings view for equality checks after a no-op PUT.""" + return copy.deepcopy(view) + + +def test_health(client: ApiClient, expected_version: str) -> str: + resp = client.get("/api/health", auth=False) + assert_status(resp, 200) + data = assert_json_object(resp) + if data.get("status") != "ok": + raise TestFailure(f"health.status={data.get('status')!r}, expected 'ok'") + if expected_version and data.get("version") != expected_version: + raise TestFailure(f"health.version={data.get('version')!r}, expected {expected_version!r}") + return f"version={data.get('version')}, control_ready={data.get('control_ready')}" + + +def test_openapi(client: ApiClient, expected_version: str) -> str: + resp = client.get("/api-docs/openapi.json", auth=False) + assert_status(resp, 200) + spec = assert_json_object(resp) + paths = spec.get("paths") + if not isinstance(paths, dict): + raise TestFailure("OpenAPI has no paths object") + missing = sorted(EXPECTED_OPENAPI_PATHS - set(paths)) + if missing: + raise TestFailure(f"OpenAPI missing 0.12.0 paths: {missing}") + forbidden = sorted(set(REMOVED_0120_PATHS) & set(paths)) + if forbidden: + raise TestFailure(f"OpenAPI still exposes removed paths: {forbidden}") + version = ((spec.get("info") or {}).get("version")) + if expected_version and version != expected_version: + raise TestFailure(f"OpenAPI info.version={version!r}, expected {expected_version!r}") + return f"paths={len(paths)}, version={version}" + + +def test_protected_auth(client: ApiClient) -> str: + if not client.token: + raise SkipTest("no token supplied; controller may be in trusted-LAN mode") + resp = client.get("/api/bootstrap", auth=False) + assert_status(resp, 401) + return "protected endpoint rejects missing token" + + +def test_safe_get(client: ApiClient, path: str) -> str: + resp = client.get(path) + assert_status(resp, 200) + content_type = resp.headers.get("content-type", "") + if "json" not in content_type.lower(): + # Export is still expected to be JSON, but tolerate servers that return octet-stream attachment. + if path != "/api/configuration/export": + raise TestFailure(f"unexpected content-type {content_type!r}") + data = resp.json() + if path == "/api/configuration/export": + if not isinstance(data, dict): + raise TestFailure("configuration export is not a JSON object") + fmt = data.get("format_version") + if fmt != 3: + raise TestFailure(f"configuration export format_version={fmt!r}, expected 3") + return f"backup format_version={fmt}" + if not isinstance(data, (list, dict)): + raise TestFailure(f"unexpected JSON type {type(data).__name__}") + if isinstance(data, list): + return f"items={len(data)}" + return f"keys={len(data)}" + + +def test_ha_safe_get(client: ApiClient, path: str) -> str: + resp = client.get(path) + if not client.token: + # Restricted HA API always requires a token, even when the normal admin + # API runs in trusted-LAN mode. + assert_status(resp, 401) + return "restricted HA auth enforced (no token supplied)" + assert_status(resp, 200) + data = resp.json() + if not isinstance(data, (list, dict)): + raise TestFailure(f"unexpected JSON type {type(data).__name__}") + if isinstance(data, list): + return f"items={len(data)}" + return f"keys={len(data)}" + + +def test_settings_get(client: ApiClient, path: str, required: set[str]) -> str: + resp = client.get(path) + assert_status(resp, 200) + data = assert_json_object(resp) + missing = sorted(required - set(data)) + if missing: + raise TestFailure(f"missing keys: {missing}") + # Secret values must never be returned by split GETs. + forbidden_secret_keys = { + "/api/settings/influxdb": {"password", "token"}, + "/api/settings/notifications": { + "pushover_app_token", "pushover_user_key", "slack_webhook_url", "discord_webhook_url" + }, + "/api/settings/home-assistant": {"token"}, + }.get(path, set()) + leaked = sorted(forbidden_secret_keys & set(data)) + if leaked: + raise TestFailure(f"secret fields leaked in GET response: {leaked}") + return f"keys={len(data)}" + + +def test_removed_path(client: ApiClient, path: str) -> str: + # GET is enough to prove there is no compatibility alias. Some removed write-only + # paths may return 404 or 405 depending on router fallback/method handling. + resp = client.get(path) + if resp.status not in (404, 405): + raise TestFailure(f"removed endpoint still responds with HTTP {resp.status}") + return f"HTTP {resp.status}" + + +def test_detail_endpoint(client: ApiClient, collection_path: str, detail_template: str) -> str: + resp = client.get(collection_path) + assert_status(resp, 200) + collection = assert_json_collection(resp) + item = first_item(collection) + if item is None: + raise SkipTest("collection is empty") + item_id = item.get("id") + if not isinstance(item_id, str) or not item_id: + raise SkipTest("first item has no string id") + path = detail_template.replace("{id}", urllib.parse.quote(item_id, safe="")) + detail = client.get(path) + assert_status(detail, 200) + assert_json_object(detail) + return f"id={item_id}" + + +def test_flow_read_subresources(client: ApiClient) -> str: + resp = client.get("/api/flows") + assert_status(resp, 200) + item = first_item(assert_json_collection(resp)) + if item is None or not isinstance(item.get("id"), str): + raise SkipTest("no Flow available") + flow_id = urllib.parse.quote(item["id"], safe="") + export_resp = client.get(f"/api/flows/{flow_id}/export") + assert_status(export_resp, 200) + export_resp.json() + logs_resp = client.get(f"/api/flows/{flow_id}/logs") + assert_status(logs_resp, 200) + logs_resp.json() + return f"id={item['id']}" + + +def test_settings_roundtrip(client: ApiClient, path: str) -> str: + before_resp = client.get(path) + assert_status(before_resp, 200) + before = assert_json_object(before_resp) + payload = convert_settings_view_to_update(path, before) + + put_resp = client.put(path, payload) + assert_status(put_resp, 200) + put_view = assert_json_object(put_resp) + + after_resp = client.get(path) + assert_status(after_resp, 200) + after = assert_json_object(after_resp) + + if comparable_settings_view(path, put_view) != comparable_settings_view(path, after): + raise TestFailure(f"PUT response differs from following GET: put={put_view!r}, get={after!r}") + + # No-op write should preserve the observable GET view, including *_configured flags. + if comparable_settings_view(path, before) != comparable_settings_view(path, after): + raise TestFailure(f"settings changed after no-op round-trip: before={before!r}, after={after!r}") + return "GET -> PUT(no-op) -> GET preserved view" + + +def expect_validation_error(client: ApiClient, path: str, mutate: Callable[[dict[str, Any]], None]) -> str: + before_resp = client.get(path) + assert_status(before_resp, 200) + before = assert_json_object(before_resp) + payload = convert_settings_view_to_update(path, before) + mutate(payload) + resp = client.put(path, payload) + assert_status(resp, 400) + error = assert_json_object(resp) + if not isinstance(error.get("error"), str) or not error["error"]: + raise TestFailure(f"400 response missing string error: {error!r}") + after_resp = client.get(path) + assert_status(after_resp, 200) + after = assert_json_object(after_resp) + if before != after: + raise TestFailure("settings changed despite rejected request") + return error["error"][:120] + + +def run_suite(args: argparse.Namespace) -> int: + client = ApiClient(args.base_url, args.token, args.timeout, args.insecure) + runner = Runner(client, verbose=args.verbose) + + print(f"GREE Controller API test: {client.base_url}") + print(f"Expected version: {args.expected_version or '(any)'}") + print(f"Auth token: {'yes' if args.token else 'no'}") + print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n") + + runner.run("public health", lambda: test_health(client, args.expected_version)) + runner.run("OpenAPI 0.12.0 contract", lambda: test_openapi(client, args.expected_version)) + runner.run("protected API requires auth", lambda: test_protected_auth(client)) + + for path in SAFE_GET_PATHS: + runner.run(f"GET {path}", lambda path=path: test_safe_get(client, path)) + + for path in HA_SAFE_GET_PATHS: + runner.run(f"GET {path}", lambda path=path: test_ha_safe_get(client, path)) + + for path, required in SETTINGS_PATHS.items(): + runner.run(f"split settings GET {path}", lambda path=path, required=required: test_settings_get(client, path, required)) + + for path in REMOVED_0120_PATHS: + runner.run(f"removed route {path}", lambda path=path: test_removed_path(client, path)) + + for collection, detail in DETAIL_COLLECTIONS: + runner.run( + f"detail read {detail}", + lambda collection=collection, detail=detail: test_detail_endpoint(client, collection, detail), + ) + runner.run("Flow export/logs read", lambda: test_flow_read_subresources(client)) + + if args.settings_write: + print("\n--- settings write/validation tests ---") + for path in SETTINGS_PATHS: + runner.run(f"round-trip PUT {path}", lambda path=path: test_settings_roundtrip(client, path)) + + runner.run( + "validation: empty GREE controller_id -> 400", + lambda: expect_validation_error(client, "/api/settings/gree", lambda p: p.__setitem__("controller_id", "")), + ) + runner.run( + "validation: invalid night start_time -> 400", + lambda: expect_validation_error(client, "/api/settings/night", lambda p: p.__setitem__("start_time", "99:99")), + ) + runner.run( + "validation: invalid notification provider -> 400", + lambda: expect_validation_error( + client, "/api/settings/notifications", lambda p: p.__setitem__("provider", "invalid-provider") + ), + ) + runner.run( + "validation: invalid HA URL scheme -> 400", + lambda: expect_validation_error( + client, "/api/settings/home-assistant", lambda p: p.__setitem__("url", "ftp://invalid.local") + ), + ) + + exit_code = runner.summary() + + if args.report_json: + report_path = Path(args.report_json) + report_path.parent.mkdir(parents=True, exist_ok=True) + report = { + "base_url": client.base_url, + "expected_version": args.expected_version, + "settings_write": bool(args.settings_write), + "exit_code": exit_code, + "results": [r.__dict__ for r in runner.results], + } + report_path.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + print(f"JSON report: {report_path}") + + return exit_code + + +def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Developer smoke/integration tests for GREE Controller API 0.12.0.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""Examples: + python3 scripts/api_dev_test.py + python3 scripts/api_dev_test.py --base-url http://192.168.1.50:8787 + GREE_API_TOKEN=secret python3 scripts/api_dev_test.py --settings-write + python3 scripts/api_dev_test.py --settings-write --report-json /tmp/api-report.json + +Default mode performs only reads. --settings-write re-saves the current settings +and intentionally generates normal settings events/side effects. It does NOT send +house/device/zone control commands. +""", + ) + parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help=f"API base URL (default: {DEFAULT_BASE_URL})") + parser.add_argument("--token", default=DEFAULT_TOKEN, help="administrator app token; or set GREE_API_TOKEN") + parser.add_argument( + "--expected-version", + default=DEFAULT_EXPECTED_VERSION, + help=f"expected health/OpenAPI version; use empty string to disable (default: {DEFAULT_EXPECTED_VERSION})", + ) + parser.add_argument("--timeout", type=float, default=8.0, help="HTTP timeout in seconds (default: 8)") + parser.add_argument("--insecure", action="store_true", help="disable TLS certificate verification for HTTPS dev instances") + parser.add_argument( + "--settings-write", + action="store_true", + help="round-trip all 8 settings PUT endpoints and test selected 400 validation paths", + ) + parser.add_argument("--report-json", help="write machine-readable test results to this JSON file") + parser.add_argument("-v", "--verbose", action="store_true", help="show PASS details") + return parser.parse_args(argv) + + +def main() -> int: + return run_suite(parse_args()) + + +if __name__ == "__main__": + sys.exit(main())