This commit is contained in:
Mateusz Gruszczyński
2026-09-13 10:21:55 +02:00
parent a7fc1f2dc4
commit c3fbc5ccc6
14 changed files with 152 additions and 42 deletions
Generated
+1 -1
View File
@@ -700,7 +700,7 @@ dependencies = [
[[package]] [[package]]
name = "gree-controller" name = "gree-controller"
version = "0.13.9" version = "0.13.10"
dependencies = [ dependencies = [
"aes", "aes",
"aes-gcm", "aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "gree-controller" name = "gree-controller"
version = "0.13.9" version = "0.13.10"
edition = "2021" edition = "2021"
authors = ["GREE Controller contributors"] authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+18 -18
View File
@@ -1,16 +1,16 @@
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
36a2cb85329ae82d097a335a6afd81c1ba7bc450eebdac09c600e12befe36ba2 ./Cargo.lock 4b2973e384f48aef569a66e2da448f6a82f33adbbc8d11dd33e7c20d8552536d ./Cargo.lock
5f9afa17638997837880ab244301e3921a6444972154b49797a73bc3103fbcad ./Cargo.toml 71acebe2a2ebb705089abf834eb7b2494daff9a4c93eae3f61ed6d4b0d21ef99 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
491aeceb202b240807d0d3cb72cdae622ee9ae8cbfd89439dc4add0bc648144f ./README.md 4cf04e8f22fb4e2551ee5fe2fe96fc50dda615b42a0274ba87aba60a59f021fc ./README.md
bb549919b96b0413152dd905aec8640bb53301f07ec7d0b1be48ccbd37936fef ./build.rs bb549919b96b0413152dd905aec8640bb53301f07ec7d0b1be48ccbd37936fef ./build.rs
d590a6df7ec31c274a2437075d76305cfafbe56385c800fbf39b18b54875119e ./docs/API.md abe109ed04cd90336e2e136fc77e9cfeefd6a9818a8c354ece08e96a5edc0730 ./docs/API.md
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md 2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
3e995a5bb3ec40b26c2676818f90adbd5fa42e760b487d18a16b09a586702484 ./docs/openapi.json e3da335c88fe66baaeaf9b1f33cb69b13f47da7415bda39929d86f51174e9026 ./docs/openapi.json
fef9a676b9ce82d0539d44e456237a814169a17a158557d7b803a455985bc020 ./ha-addon/.env.example c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile
9266b7885849007ac7626c0151b401da2fcb8e0a2721a6d196089dee1a67fbf3 ./ha-addon/README.md 0417c88a990c72aecf071fba64d5d9da65dcda4d396cd7565477f7a4532c8c5e ./ha-addon/README.md
cd7fd4b937a1d6138699f7effd184e3c077912b881e9c73c37b90bbafba68477 ./ha-addon/build.sh cd7fd4b937a1d6138699f7effd184e3c077912b881e9c73c37b90bbafba68477 ./ha-addon/build.sh
fd041032f353ceb0a2b02005f5a78d74a460b12e4e78de13dcb60129859f8c4e ./ha-addon/home-assistant/README.md fd041032f353ceb0a2b02005f5a78d74a460b12e4e78de13dcb60129859f8c4e ./ha-addon/home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py
@@ -21,7 +21,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py 41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
500f87cbfac7c933a5c22f974bf15ec8ee25ed1136eac7eeafe6a2918f8cd503 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json 8fff53290ad57c49d124777083bbae9f9804e3af398c9d51d44ee29987e2d033 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -29,10 +29,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json 6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json 13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
fc6cad1fe53f1ad365cdf0a089ce4156460e6b07ba404b65b78aafd1a0a8ba01 ./ha-addon/repository/gree-controller/CHANGELOG.md 7421c100b66152f3a513abd436afbae8ced4a91968ac89159431d7237f50d2c5 ./ha-addon/repository/gree-controller/CHANGELOG.md
3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md 3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
fdfb371926690e26a3693acd32d899e222ea66e4cb3476f717f55382e303e0d0 ./ha-addon/repository/gree-controller/config.yaml 3cfb0dfc0c41d91d533a889796645c3818eb8e74a8be53610da79c88c0e88efa ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png 3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png 0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
d13e4c2b993d209078342c0c86d852b300692089b377aa044f811308dd188a1e ./ha-addon/repository/gree-controller/translations/en.yaml d13e4c2b993d209078342c0c86d852b300692089b377aa044f811308dd188a1e ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -84,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
b48677a5d38684a524c6a1b0275bb6640fa1fb87e05ba29e92470f2d0aa89957 ./scripts/api_dev_test.py 9ca0b7780699d542d6fc6fa567dc5c3f847a57dcb676b02bb83b905862a4ff73 ./scripts/api_dev_test.py
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
@@ -103,7 +103,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs 9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
368cd843dcac970f1aa6800ba132a4bbc3429e8379ed7abf9878dcb0c633af40 ./src/api/configuration.rs 368cd843dcac970f1aa6800ba132a4bbc3429e8379ed7abf9878dcb0c633af40 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
a58fb7dcf3f37e3f9d90363623ec91edb667bca97f5faa6405c8b8f539c3d998 ./src/api/devices.rs e219dafc951207cb70bc7808054a0553a03fb0a948d11af2eb5a119d1340e29e ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
44b74965f3256c2972d6b3cd384a63c68e3b422f414a0902c085bc92e17369bf ./src/api/flows.rs 44b74965f3256c2972d6b3cd384a63c68e3b422f414a0902c085bc92e17369bf ./src/api/flows.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
@@ -159,7 +159,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb
920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs 920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs
60cac63e6cb41d835e35cc654363f6ffd4386a79f562df0e656b69034acfbac2 ./src/models/control_plan.rs 60cac63e6cb41d835e35cc654363f6ffd4386a79f562df0e656b69034acfbac2 ./src/models/control_plan.rs
9cd1904a0f822c557f6ad4576cdb5f96d9668d04e79ea9783885c9f39519752e ./src/models/defaults.rs 9cd1904a0f822c557f6ad4576cdb5f96d9668d04e79ea9783885c9f39519752e ./src/models/defaults.rs
f935d35b4a409f12774209f3fa2ffe60f579a8bc1722badbb27d4766efd6368e ./src/models/device.rs fa8ddca35171680dd9afb3bc46236e0a4922a9bbbf3e8e9939991973281534d3 ./src/models/device.rs
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
cf63af00d98410a1452f205ce00b8a6c5f0f3525b994cba3e971d631db288396 ./src/models/history.rs cf63af00d98410a1452f205ce00b8a6c5f0f3525b994cba3e971d631db288396 ./src/models/history.rs
e1621255217ee6128151cc898373dd404123cb6d54017beec6c7e3e5c3030486 ./src/models/integrations.rs e1621255217ee6128151cc898373dd404123cb6d54017beec6c7e3e5c3030486 ./src/models/integrations.rs
@@ -171,13 +171,13 @@ c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/t
cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs
28969da06a454ddd1ebea5fa606e8446a83dfd21e21352767c189013c508cb90 ./src/protocol/gree.rs 28969da06a454ddd1ebea5fa606e8446a83dfd21e21352767c189013c508cb90 ./src/protocol/gree.rs
7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs 7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs
d4df52f0202eb32290e68dc945c4b0b44f3be8aa90bd57fd116eb20285960616 ./src/protocol/gree/commands.rs d71771855d5ac90fae4994debf53ed09472b216c17bb2155d80aa2de13c57e18 ./src/protocol/gree/commands.rs
4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs 4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs
322bcb70016653f8bf71cc620020aa87d4f3f56b4d800ee9425a95b1bcc67d2e ./src/protocol/gree/discovery.rs 87d8d9686a416413489d34cb4fc2e5ac4c05f7e1e7e5bc362e9351c06c89fa51 ./src/protocol/gree/discovery.rs
0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs 0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs
6c18d870f98525e87ba821d69424e6b19a22b2c06106b7fd657f30912cd2705f ./src/protocol/gree/network.rs c09c931fa8d512a8883fb14f3142be694af3c51290e023d25c7c972e156e4bd2 ./src/protocol/gree/network.rs
de37931303fc92a9399e6edc17dfabd6870a38cd2ddb27257427c76768a66ec2 ./src/protocol/gree/polling.rs 0a901ff258a74072af3812c5417196da1a9b8daa8a2eff30c96d038558601739 ./src/protocol/gree/polling.rs
c868422c02ba457df21ae92acbd00698c243855ddd1d4e95ac2166796eba9a44 ./src/protocol/gree/tests.rs 50152bcad4a843ae04000b991a2365abb302fc1583ea69f09320e25740f3c38c ./src/protocol/gree/tests.rs
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs 3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
923bb4b2d342d6c1870474e43f33b0aba16dc88590c561d765b874412e83df1f ./src/protocol/mod.rs 923bb4b2d342d6c1870474e43f33b0aba16dc88590c561d765b874412e83df1f ./src/protocol/mod.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs 830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
+3 -1
View File
@@ -2,7 +2,9 @@
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API. Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
**Current release: 0.13.9** **Current release: 0.13.10**
Release 0.13.10 improves compatibility with GREE units that report optional feature fields as multi-state values instead of strict 0/1 flags. In particular, `Quiet=2` or `Quiet=3` no longer invalidates the whole status response, and the controller remembers the active Quiet encoding reported by the unit while keeping `Quiet=1` as the legacy default. Existing AES-ECB/AES-GCM behavior and API contracts remain unchanged.
Release 0.13.9 fixes dashboard thermostat card sizing when local thermostat or manual device control is active. Cards now keep their intrinsic height instead of stretching every thermostat in the same grid row; API contracts are unchanged. Release 0.13.9 fixes dashboard thermostat card sizing when local thermostat or manual device control is active. Cards now keep their intrinsic height instead of stretching every thermostat in the same grid row; API contracts are unchanged.
+3 -3
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference # GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.13.9**. HTTP and WebSocket API for GREE Controller **0.13.10**.
[← Main documentation](../README.md) [← Main documentation](../README.md)
@@ -232,7 +232,7 @@ Response:
{ {
"status": "ok", "status": "ok",
"name": "gree-controller", "name": "gree-controller",
"version": "0.13.9", "version": "0.13.10",
"uptime_seconds": 1234, "uptime_seconds": 1234,
"control_ready": true, "control_ready": true,
"time": "2026-08-30T06:54:00Z" "time": "2026-08-30T06:54:00Z"
@@ -295,7 +295,7 @@ Returns the initial Web UI snapshot:
"control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []}, "control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
"control_plan_revision": 42, "control_plan_revision": 42,
"system": { "system": {
"version": "0.13.9", "version": "0.13.10",
"uptime_seconds": 1234, "uptime_seconds": 1234,
"auth_required": false, "auth_required": false,
"control_ready": true, "control_ready": true,
+3 -3
View File
@@ -2,9 +2,9 @@
"openapi": "3.1.0", "openapi": "3.1.0",
"info": { "info": {
"title": "GREE Controller API", "title": "GREE Controller API",
"version": "0.13.9", "version": "0.13.10",
"summary": "Local HTTP/WebSocket API for GREE HVAC control", "summary": "Local HTTP/WebSocket API for GREE HVAC control",
"description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nNormal administrator endpoints accept `Authorization: Bearer <APP_TOKEN>` or `x-api-token: <APP_TOKEN>` when `GREE_CONTROLLER_APP_TOKEN` is configured. If the app token is empty, those endpoints intentionally run in trusted-LAN mode without authentication.\n\nThe restricted `/api/integrations/home-assistant/*` control surface always requires either a generated access token or the administrator app token. Generated tokens do **not** grant normal administrator API access.\n\nIn Swagger UI use **Authorize** and enter the token in either the bearer field or `x-api-token` field.\n\n### Control ownership\nThermostat zones, groups, house rules, direct device control, schedules and visual Flows share an ownership/arbitration model. Prefer zone/group/house endpoints for thermostat intent; direct device commands are technical/manual control and can deliberately take ownership.\n\n### 0.12.0 breaking API\nRuntime settings are split into functional `/api/settings/*` resources. The former `/api/settings`, `/api/debug`, `/api/events/retention`, `/api/settings/export` and `/api/settings/import` endpoints were removed without compatibility aliases.\n\n### 0.13.0\n`control-plan` keeps its existing HTTP response shape but is materialized in memory and pushed to WebSocket clients as `control_plan.updated`; HTTP remains the fallback/resync path. A restricted Home Assistant snapshot endpoint combines devices, groups and the control plan.\n\n### 0.13.1\nRuntime WebSocket churn is reduced without changing contracts: no-op zone regulator heartbeats are deduplicated and device heartbeat polls only invalidate the materialized control plan when plan-relevant device state changes.\n\n### 0.13.2\nCompletes live WebSocket synchronization for entity creation, schedules, automations, configuration imports and runtime device/automation state while preserving the existing HTTP API contracts and control-plan fallback behavior.\n\n### 0.13.3\nLogical thermostat targets are normalized to 0.1 C consistently, including Temporary Quick Thermostat and thermostat-managed automation/Flow actions. The bundled Web UI accepts comma or dot decimal input for the temporary thermostat target; physical GREE setpoint rounding is unchanged.\n\n### 0.13.4\nDisabled-zone thermostat controls remain visually active in the bundled Web UI because explicit temporary/manual thermostat and direct manual control remain available while zone automation is disabled. HTTP and WebSocket contracts are unchanged.\n\n### 0.13.5\nThe zone settings dialog now explains directly beside the enabled/disabled option that disabling a zone stops automation but does not block manual thermostat, temporary thermostat or direct manual control. HTTP and WebSocket contracts remain unchanged.\n\n### 0.13.6\nNotification settings add a dedicated sensor_discrepancy alert type for GREE vs Home Assistant room-temperature divergence. Disabling it suppresses only that notification; the GREE safety fallback remains active.\n\n### 0.13.7\nThe bootstrap snapshot now embeds all eight redacted runtime settings views. The bundled Web UI consumes those settings directly for HTTP startup and WebSocket resynchronization, removing the previous settings-request fan-out while preserving the split settings endpoints for direct reads and writes.\n\n### 0.13.8\nThe bundled Web UI suppresses the initial English-to-selected-language flash with a dedicated pre-paint language initializer and reveals the application only after localization is initialized. Language-pack URLs are content-versioned so immutable browser caching stays safe across updates.\n\n### 0.13.9\nThe bundled Web UI keeps dashboard thermostat cards at their intrinsic height when a local thermostat or manual device-control takeover panel is shown, preventing unrelated thermostat cards in the same grid row from stretching. HTTP and WebSocket contracts are unchanged.", "description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nNormal administrator endpoints accept `Authorization: Bearer <APP_TOKEN>` or `x-api-token: <APP_TOKEN>` when `GREE_CONTROLLER_APP_TOKEN` is configured. If the app token is empty, those endpoints intentionally run in trusted-LAN mode without authentication.\n\nThe restricted `/api/integrations/home-assistant/*` control surface always requires either a generated access token or the administrator app token. Generated tokens do **not** grant normal administrator API access.\n\nIn Swagger UI use **Authorize** and enter the token in either the bearer field or `x-api-token` field.\n\n### Control ownership\nThermostat zones, groups, house rules, direct device control, schedules and visual Flows share an ownership/arbitration model. Prefer zone/group/house endpoints for thermostat intent; direct device commands are technical/manual control and can deliberately take ownership.\n\n### 0.12.0 breaking API\nRuntime settings are split into functional `/api/settings/*` resources. The former `/api/settings`, `/api/debug`, `/api/events/retention`, `/api/settings/export` and `/api/settings/import` endpoints were removed without compatibility aliases.\n\n### 0.13.0\n`control-plan` keeps its existing HTTP response shape but is materialized in memory and pushed to WebSocket clients as `control_plan.updated`; HTTP remains the fallback/resync path. A restricted Home Assistant snapshot endpoint combines devices, groups and the control plan.\n\n### 0.13.1\nRuntime WebSocket churn is reduced without changing contracts: no-op zone regulator heartbeats are deduplicated and device heartbeat polls only invalidate the materialized control plan when plan-relevant device state changes.\n\n### 0.13.2\nCompletes live WebSocket synchronization for entity creation, schedules, automations, configuration imports and runtime device/automation state while preserving the existing HTTP API contracts and control-plan fallback behavior.\n\n### 0.13.3\nLogical thermostat targets are normalized to 0.1 C consistently, including Temporary Quick Thermostat and thermostat-managed automation/Flow actions. The bundled Web UI accepts comma or dot decimal input for the temporary thermostat target; physical GREE setpoint rounding is unchanged.\n\n### 0.13.4\nDisabled-zone thermostat controls remain visually active in the bundled Web UI because explicit temporary/manual thermostat and direct manual control remain available while zone automation is disabled. HTTP and WebSocket contracts are unchanged.\n\n### 0.13.5\nThe zone settings dialog now explains directly beside the enabled/disabled option that disabling a zone stops automation but does not block manual thermostat, temporary thermostat or direct manual control. HTTP and WebSocket contracts remain unchanged.\n\n### 0.13.6\nNotification settings add a dedicated sensor_discrepancy alert type for GREE vs Home Assistant room-temperature divergence. Disabling it suppresses only that notification; the GREE safety fallback remains active.\n\n### 0.13.7\nThe bootstrap snapshot now embeds all eight redacted runtime settings views. The bundled Web UI consumes those settings directly for HTTP startup and WebSocket resynchronization, removing the previous settings-request fan-out while preserving the split settings endpoints for direct reads and writes.\n\n### 0.13.8\nThe bundled Web UI suppresses the initial English-to-selected-language flash with a dedicated pre-paint language initializer and reveals the application only after localization is initialized. Language-pack URLs are content-versioned so immutable browser caching stays safe across updates.\n\n### 0.13.9\nThe bundled Web UI keeps dashboard thermostat cards at their intrinsic height when a local thermostat or manual device-control takeover panel is shown, preventing unrelated thermostat cards in the same grid row from stretching. HTTP and WebSocket contracts are unchanged.\n\n### 0.13.10\nGREE status parsing now tolerates multi-state non-zero encodings for optional feature flags. `Quiet` preserves active wire values 1/2/3 reported by the unit and reuses the observed encoding when enabled, while legacy behavior still defaults to `Quiet=1`. Existing API contracts are unchanged.",
"license": { "license": {
"name": "MIT" "name": "MIT"
} }
@@ -4891,7 +4891,7 @@
}, },
"version": { "version": {
"type": "string", "type": "string",
"example": "0.13.9" "example": "0.13.10"
}, },
"uptime_seconds": { "uptime_seconds": {
"type": "integer", "type": "integer",
+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Developer smoke/integration tests for GREE Controller API 0.13.9. """Developer smoke/integration tests for GREE Controller API 0.13.10.
Default mode is read-only and safe to run against a real controller. 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 Use --settings-write to additionally round-trip all split settings resources and
@@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional
DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787") DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787")
DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "") DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "")
DEFAULT_EXPECTED_VERSION = "0.13.9" DEFAULT_EXPECTED_VERSION = "0.13.10"
SETTINGS_PATHS: dict[str, set[str]] = { SETTINGS_PATHS: dict[str, set[str]] = {
"/api/settings/application": {"simulator_enabled"}, "/api/settings/application": {"simulator_enabled"},
@@ -553,7 +553,7 @@ def run_suite(args: argparse.Namespace) -> int:
print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n") 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("public health", lambda: test_health(client, args.expected_version))
runner.run("OpenAPI 0.13.9 contract", lambda: test_openapi(client, args.expected_version)) runner.run("OpenAPI 0.13.10 contract", lambda: test_openapi(client, args.expected_version))
runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client)) runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client))
runner.run("protected API requires auth", lambda: test_protected_auth(client)) runner.run("protected API requires auth", lambda: test_protected_auth(client))
@@ -622,7 +622,7 @@ def run_suite(args: argparse.Namespace) -> int:
def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace: def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Developer smoke/integration tests for GREE Controller API 0.13.9.", description="Developer smoke/integration tests for GREE Controller API 0.13.10.",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Examples: epilog="""Examples:
python3 scripts/api_dev_test.py python3 scripts/api_dev_test.py
+1
View File
@@ -112,6 +112,7 @@ async fn add_device(
swing_vertical: false, swing_vertical: false,
swing_horizontal: false, swing_horizontal: false,
quiet: false, quiet: false,
quiet_wire_value: None,
turbo: false, turbo: false,
light: true, light: true,
air: false, air: false,
+9
View File
@@ -34,6 +34,14 @@ pub struct Device {
pub swing_horizontal: bool, pub swing_horizontal: bool,
#[serde(default)] #[serde(default)]
pub quiet: bool, pub quiet: bool,
/// Raw non-zero value last reported by the unit for the GREE `Quiet` property.
///
/// Most of the application intentionally exposes Quiet as a boolean, but the wire
/// protocol is not consistently boolean across GREE firmware. Known units report
/// values 1, 2 or 3 for active Quiet modes. Remembering the observed value lets us
/// preserve the device-specific encoding when a later command enables Quiet again.
#[serde(default)]
pub quiet_wire_value: Option<u8>,
#[serde(default)] #[serde(default)]
pub turbo: bool, pub turbo: bool,
#[serde(default)] #[serde(default)]
@@ -106,6 +114,7 @@ impl Device {
swing_vertical: false, swing_vertical: false,
swing_horizontal: false, swing_horizontal: false,
quiet: false, quiet: false,
quiet_wire_value: None,
turbo: false, turbo: false,
light: true, light: true,
air: false, air: false,
+16 -4
View File
@@ -20,13 +20,17 @@ impl GreeClient {
command: &DeviceCommand, command: &DeviceCommand,
suppress_beep: bool, suppress_beep: bool,
) -> Result<Value> { ) -> Result<Value> {
// Keep legacy behaviour (Quiet=1) until a device proves that it uses another
// active encoding. Once polling observes Quiet=2/3, persist and reuse that value.
// This makes newer/multi-state units work without changing working older units.
let quiet_on_value = device.quiet_wire_value.unwrap_or(1).clamp(1, 3);
let try_buzzer_suppression = suppress_beep let try_buzzer_suppression = suppress_beep
&& self && self
.buzzer_unsupported .buzzer_unsupported
.lock() .lock()
.map(|items| !items.contains(&device.id)) .map(|items| !items.contains(&device.id))
.unwrap_or(true); .unwrap_or(true);
let inner = Self::command_payload(command, try_buzzer_suppression)?; let inner = Self::command_payload(command, try_buzzer_suppression, quiet_on_value)?;
match self match self
.request(device, &inner, key, false, device.protocol_version) .request(device, &inner, key, false, device.protocol_version)
.await .await
@@ -35,7 +39,7 @@ impl GreeClient {
Err(first_err) if try_buzzer_suppression => { Err(first_err) if try_buzzer_suppression => {
// Some firmwares reject unknown buzzer properties instead of ignoring them. // Some firmwares reject unknown buzzer properties instead of ignoring them.
// Retry the exact state change without buzzer fields and remember the fallback. // Retry the exact state change without buzzer fields and remember the fallback.
let fallback = Self::command_payload(command, false)?; let fallback = Self::command_payload(command, false, quiet_on_value)?;
match self match self
.request(device, &fallback, key, false, device.protocol_version) .request(device, &fallback, key, false, device.protocol_version)
.await .await
@@ -159,7 +163,11 @@ impl GreeClient {
} }
} }
fn command_payload(command: &DeviceCommand, suppress_beep: bool) -> Result<Value> { fn command_payload(
command: &DeviceCommand,
suppress_beep: bool,
quiet_on_value: u8,
) -> Result<Value> {
let mut opt = Vec::<&str>::new(); let mut opt = Vec::<&str>::new();
let mut values = Vec::<Value>::new(); let mut values = Vec::<Value>::new();
if let Some(v) = command.power { if let Some(v) = command.power {
@@ -191,7 +199,11 @@ impl GreeClient {
} }
if let Some(v) = command.quiet { if let Some(v) = command.quiet {
opt.push("Quiet"); opt.push("Quiet");
values.push(json!(if v { 1 } else { 0 })); values.push(json!(if v {
quiet_on_value.clamp(1, 3)
} else {
0
}));
} }
if let Some(v) = command.turbo { if let Some(v) = command.turbo {
opt.push("Tur"); opt.push("Tur");
+1
View File
@@ -199,6 +199,7 @@ impl GreeClient {
swing_vertical: false, swing_vertical: false,
swing_horizontal: false, swing_horizontal: false,
quiet: false, quiet: false,
quiet_wire_value: None,
turbo: false, turbo: false,
light: true, light: true,
air: false, air: false,
+13
View File
@@ -108,6 +108,19 @@ fn status_flag(name: &str, value: &Value) -> Result<bool> {
other => bail!("invalid GREE flag value for {name}: {other}"), other => bail!("invalid GREE flag value for {name}: {other}"),
} }
} }
/// Optional GREE capabilities are not uniformly encoded as strict 0/1 flags.
/// For example Quiet is known to use 1, 2 and 3 on different units/firmware,
/// and other optional properties can also expose multi-state non-zero values.
/// The public application model is boolean, so keep 0 = off and treat any
/// positive value as active without allowing one optional field to invalidate
/// the complete climate status frame.
fn status_feature_flag(name: &str, value: &Value) -> Result<bool> {
let raw = status_i64(name, value)?;
if raw < 0 {
bail!("invalid GREE feature value for {name}: {raw}")
}
Ok(raw != 0)
}
fn mode_name_checked(value: i64) -> Option<&'static str> { match value { 0 => Some("auto"), 1 => Some("cool"), 2 => Some("dry"), 3 => Some("fan"), 4 => Some("heat"), _ => None } } fn mode_name_checked(value: i64) -> Option<&'static str> { match value { 0 => Some("auto"), 1 => Some("cool"), 2 => Some("dry"), 3 => Some("fan"), 4 => Some("heat"), _ => None } }
fn mode_value(value: &str) -> Result<i64> { fn mode_value(value: &str) -> Result<i64> {
match value.to_ascii_lowercase().as_str() { match value.to_ascii_lowercase().as_str() {
+17 -7
View File
@@ -196,31 +196,41 @@ impl GreeClient {
"SwUpDn" => next.swing_vertical = status_i64(name, value)? != 0, "SwUpDn" => next.swing_vertical = status_i64(name, value)? != 0,
"SwingLfRig" => next.swing_horizontal = status_i64(name, value)? != 0, "SwingLfRig" => next.swing_horizontal = status_i64(name, value)? != 0,
"Quiet" => { "Quiet" => {
next.quiet = status_flag(name, value)?; let raw = status_i64(name, value)?;
if raw < 0 {
bail!("invalid GREE feature value for {name}: {raw}")
}
next.quiet = raw != 0;
// Quiet is a multi-state field on some GREE families. Preserve known
// active encodings so a later boolean "enable Quiet" command does not
// silently change mode (e.g. a unit that reports/uses Quiet=2).
if (1..=3).contains(&raw) {
next.quiet_wire_value = Some(raw as u8);
}
next.supports_quiet = Some(true); next.supports_quiet = Some(true);
} }
"Tur" => { "Tur" => {
next.turbo = status_flag(name, value)?; next.turbo = status_feature_flag(name, value)?;
next.supports_turbo = Some(true); next.supports_turbo = Some(true);
} }
"Lig" => { "Lig" => {
next.light = status_flag(name, value)?; next.light = status_feature_flag(name, value)?;
next.supports_light = Some(true); next.supports_light = Some(true);
} }
"Air" => { "Air" => {
next.air = status_flag(name, value)?; next.air = status_feature_flag(name, value)?;
next.supports_air = Some(true); next.supports_air = Some(true);
} }
"Blo" => { "Blo" => {
next.xfan = status_flag(name, value)?; next.xfan = status_feature_flag(name, value)?;
next.supports_xfan = Some(true); next.supports_xfan = Some(true);
} }
"Health" => { "Health" => {
next.health = status_flag(name, value)?; next.health = status_feature_flag(name, value)?;
next.supports_health = Some(true); next.supports_health = Some(true);
} }
"SwhSlp" => { "SwhSlp" => {
next.sleep = status_flag(name, value)?; next.sleep = status_feature_flag(name, value)?;
next.supports_sleep = Some(true); next.supports_sleep = Some(true);
} }
"TemSen" => { "TemSen" => {
+62
View File
@@ -37,6 +37,7 @@ mod tests {
..DeviceCommand::default() ..DeviceCommand::default()
}, },
false, false,
1,
) )
.expect("thermostat command payload"); .expect("thermostat command payload");
@@ -49,4 +50,65 @@ mod tests {
Some(serde_json::json!([19, 1, 1, 1])) Some(serde_json::json!([19, 1, 1, 1]))
); );
} }
#[test]
fn quiet_status_accepts_multistate_value_and_remembers_wire_mode() {
let client = GreeClient::new(
"test-controller".into(),
None,
None,
Arc::new(AtomicBool::new(false)),
);
let mut device = Device::simulated_default();
device.quiet = false;
device.quiet_wire_value = None;
let response = json!({
"cols": ["Pow", "Quiet"],
"dat": [1, 2]
});
client.apply_status(&mut device, &response).expect("Quiet=2 status");
assert!(device.power);
assert!(device.quiet);
assert_eq!(device.quiet_wire_value, Some(2));
assert_eq!(device.supports_quiet, Some(true));
}
#[test]
fn quiet_command_can_preserve_learned_wire_mode() {
let payload = GreeClient::command_payload(
&DeviceCommand {
quiet: Some(true),
..DeviceCommand::default()
},
false,
2,
)
.expect("quiet command payload");
assert_eq!(payload.get("opt").cloned(), Some(serde_json::json!(["Quiet"])));
assert_eq!(payload.get("p").cloned(), Some(serde_json::json!([2])));
}
#[test]
fn optional_multistate_feature_does_not_reject_whole_status() {
let client = GreeClient::new(
"test-controller".into(),
None,
None,
Arc::new(AtomicBool::new(false)),
);
let mut device = Device::simulated_default();
let response = json!({
"cols": ["Pow", "Air"],
"dat": [1, 3]
});
client.apply_status(&mut device, &response).expect("Air=3 status");
assert!(device.power);
assert!(device.air);
assert_eq!(device.supports_air, Some(true));
}
} }