v0.12.0-api

This commit is contained in:
Mateusz Gruszczyński
2026-09-03 13:38:27 +02:00
parent abcd84f5ef
commit 351ea765fa
3 changed files with 636 additions and 32 deletions
+25 -19
View File
@@ -1,16 +1,17 @@
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example 41dfdc6d099b54f87d4dd51f696122b3c88d3bf3ddf420bea8f4bc621ba920b0 ./build.rs
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
6877eacd3931f7922f3f379d46e8cd9de7a609a65060ce2285c09b9c95f230aa ./Cargo.lock 6877eacd3931f7922f3f379d46e8cd9de7a609a65060ce2285c09b9c95f230aa ./Cargo.lock
ebe64386eb553c298994df40f73a56b4980eedcd88a84cb310134070c1753eb3 ./Cargo.toml ebe64386eb553c298994df40f73a56b4980eedcd88a84cb310134070c1753eb3 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 537cb63c8f8420cc57656333d607c2ae6c908082253c831de8945c4c2dee7e4c ./data/gree-controller.db
92633f07b839dcdeafa96cb3ffdaaa600f65387a9113ed595987e68982fed0d6 ./README.md 83ac2f5e7ea116c28873a295d19e27a6e50449ac44d66b3650e559fc17ad0d81 ./data/gree-controller.db-shm
41dfdc6d099b54f87d4dd51f696122b3c88d3bf3ddf420bea8f4bc621ba920b0 ./build.rs 45a4e65ecc096d62c79a7eab907776f65c5bda4bc2248ba3348726cb2bb89f37 ./data/gree-controller.db-wal
3596e68d53d614ff784cdaa7a26eabd651cf02909da770bc863518f1218299a0 ./docs/API.md 3596e68d53d614ff784cdaa7a26eabd651cf02909da770bc863518f1218299a0 ./docs/API.md
1b522e1b61643733accf0a45b8b862cfb96e1468343c9819600e263fe67a2b96 ./docs/CODE_AUDIT_0.12.0.md 1b522e1b61643733accf0a45b8b862cfb96e1468343c9819600e263fe67a2b96 ./docs/CODE_AUDIT_0.12.0.md
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md 2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
3ba861e16d523f962574334ed74d53d6be2669dcb05851578d7a5434e8856f4e ./docs/openapi.json 3ba861e16d523f962574334ed74d53d6be2669dcb05851578d7a5434e8856f4e ./docs/openapi.json
a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
8d48f0962dca59fb590f37f112791cc876a81efea815691dca9feb61d16ca578 ./gree-controller.zip
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py 6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
71c8058f85fd9ce9f063b7a84dcf2956f23cdbbd02bd4bd99d2fbb9d177c72ad ./home-assistant/custom_components/gree_controller/climate.py 71c8058f85fd9ce9f063b7a84dcf2956f23cdbbd02bd4bd99d2fbb9d177c72ad ./home-assistant/custom_components/gree_controller/climate.py
5e4aef2143e81bedb5a15dd4be5c71b64a3ab448ec6d33a20851edd098e5f529 ./home-assistant/custom_components/gree_controller/config_flow.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 ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png 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 c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
9538b4a08e6c76dbd5850f3720f582253fd8e4542871891a2cf5552759fb334a ./home-assistant/custom_components/gree_controller/manifest.json 9538b4a08e6c76dbd5850f3720f582253fd8e4542871891a2cf5552759fb334a ./home-assistant/custom_components/gree_controller/manifest.json
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py 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 6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./home-assistant/custom_components/gree_controller/translations/en.json
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json 13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json 4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md
2444b048c40ccb3ebbc161663ccff8409f8d6763858943075c033ea8f832fdef ./lang/en.json 2444b048c40ccb3ebbc161663ccff8409f8d6763858943075c033ea8f832fdef ./lang/en.json
1b3eee0b9ece41012535bfbe07ddd7c228aa161d1aa0bc77850a39177a062928 ./lang/pl.json 1b3eee0b9ece41012535bfbe07ddd7c228aa161d1aa0bc77850a39177a062928 ./lang/pl.json
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
1e7b1bcf4194abfb8bad0d601268af0547d58541731e98b74a3500d7629d0be0 ./mock/index.html 1e7b1bcf4194abfb8bad0d601268af0547d58541731e98b74a3500d7629d0be0 ./mock/index.html
007c78e0cb404778985980b23bb5f8bbc7a8f04c80015204e24c321970e3b15e ./mock/mock.js 007c78e0cb404778985980b23bb5f8bbc7a8f04c80015204e24c321970e3b15e ./mock/mock.js
@@ -72,9 +76,9 @@ fffa18bd989a0dcc1c5458975522517d40af4af40cc1ca30640e79d3db711053 ./presets/unoc
ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/weekend_comfort.json ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/weekend_comfort.json
fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
92633f07b839dcdeafa96cb3ffdaaa600f65387a9113ed595987e68982fed0d6 ./README.md
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
e8fc6cb061f27fb81b069de1f08cfa2d3eb26557ec2221abda64b621b8092750 ./scripts/FILE_MANIFEST.sha256 966c7b81c07c07d9073f797388f695164095a520753910eb26c5daa64caaae02 ./scripts/api_dev_test.py
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -82,12 +86,12 @@ bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/READ
e4849261fd9ed1f01df96c0637c439c0c4eff8fa317b2918026167bba343af79 ./scripts/install-lxc.sh e4849261fd9ed1f01df96c0637c439c0c4eff8fa317b2918026167bba343af79 ./scripts/install-lxc.sh
bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/install.sh bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/install.sh
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
7e8f558b284362f55f50bd7223a89754a2a4960e746a27035cc61180f4a2da6f ./scripts/smoke.sh 7e8f558b284362f55f50bd7223a89754a2a4960e746a27035cc61180f4a2da6f ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
0790b7b388467d9e75b621188cca473e22d73e01bcfa74e92fa23f92b0695855 ./scripts/verify_flow_logic.py 0790b7b388467d9e75b621188cca473e22d73e01bcfa74e92fa23f92b0695855 ./scripts/verify_flow_logic.py
11a2bca9c4f349fb5fb5090e02607f26bddf692a739fea0384779820935fcea7 ./src/api.rs
825370f409124a719043200742c01c3441bc65f483108106140d13fe7bbcba91 ./src/api/assets.rs 825370f409124a719043200742c01c3441bc65f483108106140d13fe7bbcba91 ./src/api/assets.rs
6c34b1294d76b0eba4c56bfd0bf69bdd502de0e40838104f6ddf2db4e922683d ./src/api/auth.rs 6c34b1294d76b0eba4c56bfd0bf69bdd502de0e40838104f6ddf2db4e922683d ./src/api/auth.rs
63cb241c4536abed402f2a0ad37e7f299343df264eab6a33bb5354c15421e045 ./src/api/automations.rs 63cb241c4536abed402f2a0ad37e7f299343df264eab6a33bb5354c15421e045 ./src/api/automations.rs
@@ -102,13 +106,14 @@ d397e06b2034490b1238a8cf4663661a4e68c34d18a3037169e7332bd9e79cf2 ./src/api/hous
88e04a59bfedd5e6c54e6a2938b1031964be3a1505db2ca67d6290aa042cbe3b ./src/api/integrations.rs 88e04a59bfedd5e6c54e6a2938b1031964be3a1505db2ca67d6290aa042cbe3b ./src/api/integrations.rs
bb2a746ecdcc2da5fe54e18b455c7bd19453486dd5c008e71951a2c81d0e7d64 ./src/api/middleware.rs bb2a746ecdcc2da5fe54e18b455c7bd19453486dd5c008e71951a2c81d0e7d64 ./src/api/middleware.rs
45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs 45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs
50cfd47e44e22cc802f22f97267c157fa5a6d19b51be921a6ca6b1ea7c9799d1 ./src/api/public_settings.rs
11a2bca9c4f349fb5fb5090e02607f26bddf692a739fea0384779820935fcea7 ./src/api.rs
a1d1a4ac071493b052e6bf20b03be67cf75726de95649e9a73aeba8d22395a15 ./src/api/schedules.rs a1d1a4ac071493b052e6bf20b03be67cf75726de95649e9a73aeba8d22395a15 ./src/api/schedules.rs
c1275fb8fcd2b2ba24e9c6b1234f25ce6865517c792e97ad46e14ec8bb16ed83 ./src/api/settings.rs c1275fb8fcd2b2ba24e9c6b1234f25ce6865517c792e97ad46e14ec8bb16ed83 ./src/api/settings.rs
6f53b89c96a99a85c3badd6af169fda9c978630516279b57b756b5468caef7d7 ./src/api/system.rs 6f53b89c96a99a85c3badd6af169fda9c978630516279b57b756b5468caef7d7 ./src/api/system.rs
681cccb8d09f4ad9c2e6467a4dbe2f9d991125775a5d2bfc9043daf940fecd29 ./src/api/websocket.rs 681cccb8d09f4ad9c2e6467a4dbe2f9d991125775a5d2bfc9043daf940fecd29 ./src/api/websocket.rs
525c4e546a611d26006f6414fc88ab4c6773171ed83bd1ca85644945139b5b06 ./src/api/zones.rs 525c4e546a611d26006f6414fc88ab4c6773171ed83bd1ca85644945139b5b06 ./src/api/zones.rs
3cec9426989034a6a5074aead64e8c0239ae942a96a04910c57d1593129c5bb8 ./src/config.rs 3cec9426989034a6a5074aead64e8c0239ae942a96a04910c57d1593129c5bb8 ./src/config.rs
c8b8b4a04c94b9d91f5931057d69ad38344730bffbf68e2f6541b59013441b57 ./src/db.rs
24580b279cfcd5388ba584eaba2c9bb729b8aae81e77b781c23fb804c5b5fa5d ./src/db/climate.rs 24580b279cfcd5388ba584eaba2c9bb729b8aae81e77b781c23fb804c5b5fa5d ./src/db/climate.rs
6998f88a6a030ef9a1fa3a46466f6dfb79c6df086411d94c82ecd69fa1690e71 ./src/db/configuration.rs 6998f88a6a030ef9a1fa3a46466f6dfb79c6df086411d94c82ecd69fa1690e71 ./src/db/configuration.rs
56b0efe84dbf6bd3ee2397b099220a68966311c187f4d5b53007a1693a5ef327 ./src/db/core_devices.rs 56b0efe84dbf6bd3ee2397b099220a68966311c187f4d5b53007a1693a5ef327 ./src/db/core_devices.rs
@@ -116,10 +121,10 @@ d8a2b323e864f2ec38ee15e3023be37a6aa05387f1ddbeedafe1f51556ad8c7d ./src/db/devic
f3bd91d0bdb7699f319b9341d928d3cc3b25f759653fcd2095a5c5f695d08fac ./src/db/events_tokens.rs f3bd91d0bdb7699f319b9341d928d3cc3b25f759653fcd2095a5c5f695d08fac ./src/db/events_tokens.rs
03d689fb03c4f61629fff8404b6cea7101ba3a5358247b16ad714e0be93e9150 ./src/db/flows.rs 03d689fb03c4f61629fff8404b6cea7101ba3a5358247b16ad714e0be93e9150 ./src/db/flows.rs
59912e70b688daf5c4d22679d643ed3b724c262d62466aeaa9ef74db87808178 ./src/db/ha_history.rs 59912e70b688daf5c4d22679d643ed3b724c262d62466aeaa9ef74db87808178 ./src/db/ha_history.rs
c8b8b4a04c94b9d91f5931057d69ad38344730bffbf68e2f6541b59013441b57 ./src/db.rs
7e2fbce70f40aef360534ba38c6700163db77cb7288b3bf58728fce72a126435 ./src/db/schedules_automations.rs 7e2fbce70f40aef360534ba38c6700163db77cb7288b3bf58728fce72a126435 ./src/db/schedules_automations.rs
35cbb521a8ec456756496b8aee71f9dfebad528a2cf2653ba8b48f6035be0c01 ./src/db/tests.rs 35cbb521a8ec456756496b8aee71f9dfebad528a2cf2653ba8b48f6035be0c01 ./src/db/tests.rs
a384ea4042d1d11692b5f8693df1f54850dcc4e80726780b44b2b096bf667b78 ./src/db/zone_history.rs a384ea4042d1d11692b5f8693df1f54850dcc4e80726780b44b2b096bf667b78 ./src/db/zone_history.rs
fbebb9815bdabbad9ff4e979f8ea9dc3dda9c77790f30a64fa0b392fb1944d00 ./src/engine.rs
ec6034df6667b3ac6ec39851443048a56e87fa861f5846950a5e79fa89ee5a30 ./src/engine/automations.rs ec6034df6667b3ac6ec39851443048a56e87fa861f5846950a5e79fa89ee5a30 ./src/engine/automations.rs
77931af524e9402fc826050b5fcec422d01b02b625c8ce7f2004f1f6520c9b4d ./src/engine/commands.rs 77931af524e9402fc826050b5fcec422d01b02b625c8ce7f2004f1f6520c9b4d ./src/engine/commands.rs
750c5c219c7c8a72f51eb6dfc679d21c5821c122861d9cfbaba57485b66a33a4 ./src/engine/control_plan.rs 750c5c219c7c8a72f51eb6dfc679d21c5821c122861d9cfbaba57485b66a33a4 ./src/engine/control_plan.rs
@@ -129,6 +134,7 @@ c1410aae910b8239e48857df1f0484f08261ad6cf385b596f7c538e920907437 ./src/engine/g
4972c199274647bff47122c367e51bae8dab24d7ec4d652288e227a5a64c2310 ./src/engine/local_thermostat.rs 4972c199274647bff47122c367e51bae8dab24d7ec4d652288e227a5a64c2310 ./src/engine/local_thermostat.rs
abf9eb5c2444f540f1601682d4c7f3e86f6742690f6af621999406cd81f6a2e4 ./src/engine/ownership.rs abf9eb5c2444f540f1601682d4c7f3e86f6742690f6af621999406cd81f6a2e4 ./src/engine/ownership.rs
9c37adf514c96840fd6a5c715c579350fa86d6dcc0655a2aa97d1fd7c7e04299 ./src/engine/polling.rs 9c37adf514c96840fd6a5c715c579350fa86d6dcc0655a2aa97d1fd7c7e04299 ./src/engine/polling.rs
fbebb9815bdabbad9ff4e979f8ea9dc3dda9c77790f30a64fa0b392fb1944d00 ./src/engine.rs
3d48608a065aa52a1fd8b9c59356a74d8216ecee3d47f8ff8ef7d3d47170c617 ./src/engine/runtime.rs 3d48608a065aa52a1fd8b9c59356a74d8216ecee3d47f8ff8ef7d3d47170c617 ./src/engine/runtime.rs
ffa72b93502eece187a96adb4ec94aec6048d4a6dc7af66e7c7af0931318787f ./src/engine/schedules.rs ffa72b93502eece187a96adb4ec94aec6048d4a6dc7af66e7c7af0931318787f ./src/engine/schedules.rs
7742046c0067c7cd5a74e2b81f8a8bdef9b76fcacf9b4af0b7eede1b050b9036 ./src/engine/targets.rs 7742046c0067c7cd5a74e2b81f8a8bdef9b76fcacf9b4af0b7eede1b050b9036 ./src/engine/targets.rs
@@ -139,12 +145,11 @@ a9188b588b2617ce1547a45d46141f7e5f1bb0027a48675dd86b274f807d0875 ./src/engine/t
7db75c2d40df2af3759d0bd242133612d4ff768c8f26db0009853d3dcad5ade6 ./src/engine/zone_control.rs 7db75c2d40df2af3759d0bd242133612d4ff768c8f26db0009853d3dcad5ade6 ./src/engine/zone_control.rs
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs 4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
9eef7820fcf0c98b08fb1bf5e0a4a244b1dbdfa700d8b930d3ff40379babdada ./src/home_assistant.rs 9eef7820fcf0c98b08fb1bf5e0a4a244b1dbdfa700d8b930d3ff40379babdada ./src/home_assistant.rs
6f9ef85cd53ba030b477bc3c186f0f7e762674124210eabcb0686f2806eaada1 ./src/influxdb.rs
feb50ecd60b9a9fe9de1f7d8e1e26caab5435d59347468f6266a318f431bc3d3 ./src/influxdb/codec.rs feb50ecd60b9a9fe9de1f7d8e1e26caab5435d59347468f6266a318f431bc3d3 ./src/influxdb/codec.rs
553b44ec0321e62480059bc2451fd34ec7aabe9fc87fc717daba0c4b55679d53 ./src/influxdb/query.rs 553b44ec0321e62480059bc2451fd34ec7aabe9fc87fc717daba0c4b55679d53 ./src/influxdb/query.rs
6f9ef85cd53ba030b477bc3c186f0f7e762674124210eabcb0686f2806eaada1 ./src/influxdb.rs
5e4d94f8a010df05bae9acab72c518080d2dba94d054df95b87e492305357222 ./src/influxdb/write.rs 5e4d94f8a010df05bae9acab72c518080d2dba94d054df95b87e492305357222 ./src/influxdb/write.rs
c3ff7232a2fa1d29428872a10cfd72ee60a3a1faaa662eff6c2d8c7dffc1ed3a ./src/main.rs c3ff7232a2fa1d29428872a10cfd72ee60a3a1faaa662eff6c2d8c7dffc1ed3a ./src/main.rs
1030e49498f8d595cf8fdee60651e7fec79d78061eb8561f9b29948b28c2f95f ./src/models.rs
67f189c195426c39669178dd92d8602c519806acca7d325665076720f9fee8a9 ./src/models/automation.rs 67f189c195426c39669178dd92d8602c519806acca7d325665076720f9fee8a9 ./src/models/automation.rs
377fc9697a70a03ad35fc3701adf4dbe8687760dc90de9f5c532a4f3b612c762 ./src/models/control_plan.rs 377fc9697a70a03ad35fc3701adf4dbe8687760dc90de9f5c532a4f3b612c762 ./src/models/control_plan.rs
8228b9b6d9e8b1d13de29ce88500beacd88c8877893b7a3e41a4bd1e237ca36c ./src/models/defaults.rs 8228b9b6d9e8b1d13de29ce88500beacd88c8877893b7a3e41a4bd1e237ca36c ./src/models/defaults.rs
@@ -152,13 +157,13 @@ c3ff7232a2fa1d29428872a10cfd72ee60a3a1faaa662eff6c2d8c7dffc1ed3a ./src/main.rs
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
cfbaa853a5185c4be572588577b313a2d618837f7c07a3860d0e8d2cca04ee60 ./src/models/history.rs cfbaa853a5185c4be572588577b313a2d618837f7c07a3860d0e8d2cca04ee60 ./src/models/history.rs
552dfc1717ea88f3a23b1fb7705f1dbb0df7792ca9a6ae7ad7bd275eacef27af ./src/models/integrations.rs 552dfc1717ea88f3a23b1fb7705f1dbb0df7792ca9a6ae7ad7bd275eacef27af ./src/models/integrations.rs
1030e49498f8d595cf8fdee60651e7fec79d78061eb8561f9b29948b28c2f95f ./src/models.rs
8294749dd3af524925309c51ad641ffd32ff1fd9c164cfa71660aa88c2d13d6e ./src/models/runtime.rs 8294749dd3af524925309c51ad641ffd32ff1fd9c164cfa71660aa88c2d13d6e ./src/models/runtime.rs
887eb80a58998e90023e952c1fde67435eb843493387ae64536613b74c084534 ./src/models/settings_api.rs 887eb80a58998e90023e952c1fde67435eb843493387ae64536613b74c084534 ./src/models/settings_api.rs
e77f8cdcb5fc0e51cdbf2527a69585a2f6f9089e159d1e45ec4a48735ab0e304 ./src/models/temporary_thermostat.rs e77f8cdcb5fc0e51cdbf2527a69585a2f6f9089e159d1e45ec4a48735ab0e304 ./src/models/temporary_thermostat.rs
5b5c80e25ae2aad1b511bd3f0a82b8fbae2440abf8c656ecb90814587d0be137 ./src/models/zone.rs 5b5c80e25ae2aad1b511bd3f0a82b8fbae2440abf8c656ecb90814587d0be137 ./src/models/zone.rs
9c735d7c475f21f2f388c21be784376f7184f0bf18af1886544ef51585a3b290 ./src/notifications.rs 9c735d7c475f21f2f388c21be784376f7184f0bf18af1886544ef51585a3b290 ./src/notifications.rs
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs 7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
f1f765331469f4219a551414833c19b6de413114c22fdd0468e1f417659553c9 ./src/protocol/gree.rs
c935e441933ba8709432cc3ac33ed868043a89188d0f48ea6fbfe34b1f8936a3 ./src/protocol/gree/binding.rs c935e441933ba8709432cc3ac33ed868043a89188d0f48ea6fbfe34b1f8936a3 ./src/protocol/gree/binding.rs
563ded1758b135eb523f1fa0b1bf55fe5430ef55935b301fd6d79cbb980e1820 ./src/protocol/gree/commands.rs 563ded1758b135eb523f1fa0b1bf55fe5430ef55935b301fd6d79cbb980e1820 ./src/protocol/gree/commands.rs
5d4328199e2285016fd1941e909e5c09f7cdf9fd21ab6538c1bf8bc2f4af3ba3 ./src/protocol/gree/core.rs 5d4328199e2285016fd1941e909e5c09f7cdf9fd21ab6538c1bf8bc2f4af3ba3 ./src/protocol/gree/core.rs
@@ -166,14 +171,15 @@ c935e441933ba8709432cc3ac33ed868043a89188d0f48ea6fbfe34b1f8936a3 ./src/protocol
97284ad37767625cfc2b82a3de5c40f6eecddabe185bd0bcea7cf9b165e017f0 ./src/protocol/gree/merge.rs 97284ad37767625cfc2b82a3de5c40f6eecddabe185bd0bcea7cf9b165e017f0 ./src/protocol/gree/merge.rs
a96061fbbcadac6c2df2e9708a48dcf819b951eb147f32c7d0d285ac5ae2491b ./src/protocol/gree/network.rs a96061fbbcadac6c2df2e9708a48dcf819b951eb147f32c7d0d285ac5ae2491b ./src/protocol/gree/network.rs
7763855207c23357cb67256ad9a47f89f8313280bc1f07ccac8ae21967a5ec7d ./src/protocol/gree/polling.rs 7763855207c23357cb67256ad9a47f89f8313280bc1f07ccac8ae21967a5ec7d ./src/protocol/gree/polling.rs
f1f765331469f4219a551414833c19b6de413114c22fdd0468e1f417659553c9 ./src/protocol/gree.rs
35fc903404045129e3046b7997e941b88f98ceb42485cccc872c2e4aa530c75b ./src/protocol/gree/tests.rs 35fc903404045129e3046b7997e941b88f98ceb42485cccc872c2e4aa530c75b ./src/protocol/gree/tests.rs
55c2d615e76c9e4b08c4067ea734e81d81915144e637f3b51c19bea4fc879137 ./src/protocol/gree/transport.rs 55c2d615e76c9e4b08c4067ea734e81d81915144e637f3b51c19bea4fc879137 ./src/protocol/gree/transport.rs
a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol/mod.rs a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol/mod.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs 7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
3fb50f57d30ac23ef122c45dae4bd96c7cb7184df02d99aef00d9890b66479e2 ./src/queries/entities.rs 3fb50f57d30ac23ef122c45dae4bd96c7cb7184df02d99aef00d9890b66479e2 ./src/queries/entities.rs
2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs 2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs
c91e4b8b250b30cbe76f313b70c16244e9003bce92e02c82283a1831f91974c5 ./src/queries/maintenance.rs c91e4b8b250b30cbe76f313b70c16244e9003bce92e02c82283a1831f91974c5 ./src/queries/maintenance.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
3a49bad74a2f047893e726b5e00a6e833c228ad268bdf50cd60f786ba164ea54 ./src/queries/schema.rs 3a49bad74a2f047893e726b5e00a6e833c228ad268bdf50cd60f786ba164ea54 ./src/queries/schema.rs
f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs
6ee23fcaa62231d15006db078d4f763d1b67d2365fcc2d736d8a7da8df5ea382 ./src/state.rs 6ee23fcaa62231d15006db078d4f763d1b67d2365fcc2d736d8a7da8df5ea382 ./src/state.rs
@@ -181,7 +187,6 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
f78622afea899a5e904910217eceeeee306d5fc8094bdccc049bd84752780720 ./web/index.html f78622afea899a5e904910217eceeeee306d5fc8094bdccc049bd84752780720 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
8fb20fdba11df68feb0b096bb5b4c8f936db974aed5d345a0d01455054f31f95 ./web/js/bootstrap.js 8fb20fdba11df68feb0b096bb5b4c8f936db974aed5d345a0d01455054f31f95 ./web/js/bootstrap.js
dee7c4292889ddd721c220e86fae909f0d662b585e6d9c5d9e2f8fa85d9ed2ad ./web/js/charts.js dee7c4292889ddd721c220e86fae909f0d662b585e6d9c5d9e2f8fa85d9ed2ad ./web/js/charts.js
f75449450a179f5853b8b08c69dec93205985e0dcbc806d355fd6060237761f6 ./web/js/core.js f75449450a179f5853b8b08c69dec93205985e0dcbc806d355fd6060237761f6 ./web/js/core.js
@@ -193,10 +198,11 @@ f06557c6d338259059b990f8f79c91d68ac8ec1f245c302468f08929b5506901 ./web/js/forms
dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js
7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js 7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js
a0959f9caf84f1873e3549261de8b1a069f4df8326b17bfe1f8c3de120299875 ./web/js/navigation.js a0959f9caf84f1873e3549261de8b1a069f4df8326b17bfe1f8c3de120299875 ./web/js/navigation.js
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
2bc9cfa306cdeafff39a7bfc8c5744568e0130d88432351cba8b062178aefce3 ./web/js/realtime.js 2bc9cfa306cdeafff39a7bfc8c5744568e0130d88432351cba8b062178aefce3 ./web/js/realtime.js
c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js
bdeb55546a1dff5e5bdfde4b114d6330f025df1b62e528bf18a93ad6dd4c3e83 ./web/js/settings-ui.js
3c4a96bbc73cdfaf2f76798201f88ef43776875dce42f2b75d6a1808cef496d9 ./web/js/settings.js 3c4a96bbc73cdfaf2f76798201f88ef43776875dce42f2b75d6a1808cef496d9 ./web/js/settings.js
bdeb55546a1dff5e5bdfde4b114d6330f025df1b62e528bf18a93ad6dd4c3e83 ./web/js/settings-ui.js
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest 6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
c5d515b987d96ebfbc7593e9bac9aff64c8d54420f23cc8df5e2c84b14a69852 ./web/styles.css c5d515b987d96ebfbc7593e9bac9aff64c8d54420f23cc8df5e2c84b14a69852 ./web/styles.css
ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js
-13
View File
@@ -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
+611
View File
@@ -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())