This commit is contained in:
Mateusz Gruszczyński
2026-09-18 12:26:48 +02:00
parent 8d96ad2d38
commit 00cbe975bb
21 changed files with 231 additions and 106 deletions
Generated
+1 -1
View File
@@ -719,7 +719,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.15.6"
version = "0.15.7"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.15.6"
version = "0.15.7"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+20 -20
View File
@@ -1,14 +1,14 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
03fd19b33f6b87a3cf55957c44c619dc65b6c1ef6ab35a275f7142e063fa130d ./Cargo.lock
0f1ebc69bb39b480c603772736bbc968cdeada29efecca6a056e4ebd0b071cb7 ./Cargo.toml
bc531fc3bd260aa91bc4b3f000768d740222f6f80cd780cf2bbd4522d82d8841 ./Cargo.lock
8555c5b50711059f76fdbaae495a6a56cae16813af2efd7c69a7fde34762949d ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
252802f9b072e316a1898b5b39922788c0ed7ab769aba6e1cb33e5ac5595d7fb ./README.md
fb12007189dad1c860aeed1eacc561c54afb01204396982b42bfa68706dc583e ./SOURCE_MANIFEST.sha256
4d648f23faf4e57d2900f898ce5416dd0b2be9b842f18e67e8f6b31312c511dc ./README.md
54c111977ab409876fd32101fedac6757f3026106d71b187b34c4c36b53424e9 ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
c52fb51807e46086e6bb44dd532dde346a742d12d52ce703055f1d4f433fd253 ./docs/API.md
fe1bd7a84b16782f9ba2c3c301592b52b194698d7e71f0ba806a88ed3c5a9688 ./docs/API.md
a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
524977fb867f2b56b56df827df0b8f646e366c00fa31d62c577a5a118f76406f ./docs/openapi.json
f70388d4fd9a5b35c1e32c43a9a4b4599adc6bd458e388ac571684a9cb1dc20d ./docs/openapi.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
@@ -22,7 +22,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
45af41dd19b03ddc1d786730603ff2067bf3b3a794b7663acea8e45b5c1b7200 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
396c1e2aa1dce367bd1b773ff2e9b80e09729ca58900dd5141085bf30a47f8f0 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -30,21 +30,21 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
732265515c1d45eb11b966919cfa29ed9f600b6dd02a05e1abee8afda4151213 ./ha-addon/repository/gree-controller/CHANGELOG.md
861a1df8677b0539720978aa0e84e7ffc7dea96d37e6d99c6adca2ced0851a44 ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
d742c16e5f5f24be48f8055c2131ff335fb5072469d7d515328fd41e9c3aca34 ./ha-addon/repository/gree-controller/config.yaml
40d18f2415fb84fe83d79ec19a9c9ca20232d751e02e5713786bac4d38f715a1 ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
e6b93d3701d83a31506617a0e6f44ff32d8c24a52be038c8540c862b13546b50 ./ha-addon/repository/gree-controller/translations/pl.yaml
b54c60ceeac06d32af7797da9e83b90cb037600318e00512da523d37b94d3081 ./ha-addon/repository/repository.yaml
2153b230357aab4766398577e23a7a9ff83bb135ddc86d1b3f2cbfc852efe0a1 ./ha-addon/repository/repository.yaml
cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run.sh
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
2175f4ce8b5d994e1e4f912244b03721a93a41e2970f39826d6ea15fdaa7078e ./lang/en.json
1055d5835329c03eb6f5b8fab4ee438c76b23d6eebdadf70effa42998c03a279 ./lang/pl.json
37d24f8de343b4796a0a8cfff3a839e55bdb779add0c8119799787d87ad8b953 ./lang/en.json
1610da0b2bbb9424006647adfb95e1318b7670aaed5a2798db1251db82635317 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -85,7 +85,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
03d5ee12306c95d5ae5df4a2a3a422fe5044e0961bb2be4b7ee49641a9ee7fc1 ./scripts/api_dev_test.py
9083c4a8c602698a53d926864f2fe7e8c9658379f3d82fcb4171f44ec946a7ad ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -105,7 +105,7 @@ b53e1afab342634fedc218ea4517446f19a5ba7985dee599e191b37d9cc8058a ./src/api.rs
27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
f775aa2f3c5c554e4197d47cf381f3d2133ea4c1430a5510f045188dd41e5e51 ./src/api/devices.rs
8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
@@ -170,20 +170,20 @@ fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/c
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs
1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs
53caf9ba4667a2ae33d5c3ce69f85005d9f8540f2ee3c16c2cb3795a53e9d21f ./src/models/runtime.rs
e557ee4a443de586a7ea7dd04318694fb30d6ec053076429503e18f1bcd65c9e ./src/models/runtime.rs
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
5f9f46d3ee6d410b8848ad46f2c482159eebfd692e739b0f09941a43c8d97211 ./src/protocol/gree/binding.rs
2029bad472fdf9f73637be6b7168e9ac79f6bdb55d2bf440d4aa254f3db9659b ./src/protocol/gree/binding.rs
91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs
4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs
034344ab9b86a271854358cfc733d45386c90e8ea66e1f232afd927428422e3a ./src/protocol/gree/discovery.rs
f4e07ca05e037e29d08abb1635f2e6d475522ce517647c422e05b749cd648ad9 ./src/protocol/gree/discovery.rs
ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs
1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs
c438719392dfda8ddf1bccf4ad451038aa4f6aa8dc5477533b0aca96f3661bed ./src/protocol/gree/tests.rs
61d588dc055ac82f0f06fceac5236d5b4b7a0c0d2939144957a174bcbf2fa83c ./src/protocol/gree/tests.rs
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs
b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs
@@ -202,7 +202,7 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree
0b783d6cea599e53fed4ed82fa95e2048c493057c6d57b0dfa423e7cfa10a7ad ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
baff48bb71ba348fe39d52fe8d2bef0614bb0c53c1ff5ee748ce09458b3983b2 ./web/index.html
cc1509fd76f0b0bd4c069db63e1e104210c2bed05f32a3eebac7642f94ef8a99 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
@@ -214,7 +214,7 @@ b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynam
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
57d85a638359d720798068ef34c88237ab92ee08dcd7478055b806ff0c858aac ./web/js-dynamic/navigation.js
30cbaebb817d5d416e1a372595b647aae42c14b9cc406c4751d0ced563243b69 ./web/js-dynamic/navigation.js
0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
+4 -4
View File
@@ -2,9 +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.
**Version: 0.15.6**
**Version: 0.15.7**
Version 0.15.6 simplifies Local discovery to one API flow: scan candidates, select units, then add them. The obsolete add-all discovery endpoint and its compatibility code are removed.
Version 0.15.7 fixes mixed V1/V2 Local discovery: scan-envelope encryption is no longer mistaken for the device protocol, Auto binding verifies the real protocol, and V2-capable modules advertised through legacy discovery are classified correctly from their discovery metadata.
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
> Built-in API: `/api/*` and `/ws`; interactive Swagger docs: `/api-docs`; OpenAPI JSON: `/api-docs/openapi.json`.
@@ -185,7 +185,7 @@ Cloud REST and MQTT use TLS with certificate/hostname validation enabled. Passwo
1. Place the controller host on a network that can reach the air-conditioner Wi-Fi modules by UDP.
2. Open **Devices** and start discovery.
3. Auto protocol mode accepts both supported GREE encryption generations. Selecting V1 or V2 strictly filters discovery and bind to that generation.
3. Auto protocol mode accepts both supported GREE encryption generations. Discovery treats the reply envelope only as transport, uses the inner `ver` field as a V1/V2 hint, and verifies the real generation during bind with fallback. Selecting V1 or V2 keeps discovery and bind locked to that generation.
4. Review the discovery results (model, MAC and IP), select the units you want, then choose **Add selected**. Only selected units are persisted and bound.
5. Create a thermostat zone for each unit you want the thermostat engine to own.
@@ -193,7 +193,7 @@ Discovery uses UDP broadcast, so routed/VLAN networks must explicitly permit or
### Compatibility with newer GREE firmware
GREE Controller supports the two commonly reverse-engineered local LAN protocol generations used by compatible units: the older AES-ECB variant and the newer AES-GCM variant. Protocol auto-detection and binding already cover both generations.
GREE Controller supports the two commonly reverse-engineered local LAN protocol generations used by compatible units: the older AES-ECB variant and the newer AES-GCM variant. Some AES-GCM-capable modules still answer the common discovery scan through a legacy/plain envelope, so the envelope itself is not used as proof of V1. Auto mode uses discovery metadata as a hint and records the protocol that actually succeeds during bind.
Some newer GREE Wi-Fi modules and firmware releases have been reported to behave differently: the unit is reachable on the network and continues to work in the official GREE+ application, but does not answer the usual local UDP traffic on port `7000`. Reports include firmware branches such as `2.12` and `3.x`, although firmware numbering differs between Wi-Fi module families and should not be treated as a universal compatibility boundary.
+19 -19
View File
@@ -1,13 +1,13 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
03fd19b33f6b87a3cf55957c44c619dc65b6c1ef6ab35a275f7142e063fa130d ./Cargo.lock
0f1ebc69bb39b480c603772736bbc968cdeada29efecca6a056e4ebd0b071cb7 ./Cargo.toml
bc531fc3bd260aa91bc4b3f000768d740222f6f80cd780cf2bbd4522d82d8841 ./Cargo.lock
8555c5b50711059f76fdbaae495a6a56cae16813af2efd7c69a7fde34762949d ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
252802f9b072e316a1898b5b39922788c0ed7ab769aba6e1cb33e5ac5595d7fb ./README.md
4d648f23faf4e57d2900f898ce5416dd0b2be9b842f18e67e8f6b31312c511dc ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
c52fb51807e46086e6bb44dd532dde346a742d12d52ce703055f1d4f433fd253 ./docs/API.md
fe1bd7a84b16782f9ba2c3c301592b52b194698d7e71f0ba806a88ed3c5a9688 ./docs/API.md
a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
524977fb867f2b56b56df827df0b8f646e366c00fa31d62c577a5a118f76406f ./docs/openapi.json
f70388d4fd9a5b35c1e32c43a9a4b4599adc6bd458e388ac571684a9cb1dc20d ./docs/openapi.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
@@ -21,7 +21,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
45af41dd19b03ddc1d786730603ff2067bf3b3a794b7663acea8e45b5c1b7200 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
396c1e2aa1dce367bd1b773ff2e9b80e09729ca58900dd5141085bf30a47f8f0 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -29,21 +29,21 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
732265515c1d45eb11b966919cfa29ed9f600b6dd02a05e1abee8afda4151213 ./ha-addon/repository/gree-controller/CHANGELOG.md
861a1df8677b0539720978aa0e84e7ffc7dea96d37e6d99c6adca2ced0851a44 ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
d742c16e5f5f24be48f8055c2131ff335fb5072469d7d515328fd41e9c3aca34 ./ha-addon/repository/gree-controller/config.yaml
40d18f2415fb84fe83d79ec19a9c9ca20232d751e02e5713786bac4d38f715a1 ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
e6b93d3701d83a31506617a0e6f44ff32d8c24a52be038c8540c862b13546b50 ./ha-addon/repository/gree-controller/translations/pl.yaml
b54c60ceeac06d32af7797da9e83b90cb037600318e00512da523d37b94d3081 ./ha-addon/repository/repository.yaml
2153b230357aab4766398577e23a7a9ff83bb135ddc86d1b3f2cbfc852efe0a1 ./ha-addon/repository/repository.yaml
cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run.sh
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
2175f4ce8b5d994e1e4f912244b03721a93a41e2970f39826d6ea15fdaa7078e ./lang/en.json
1055d5835329c03eb6f5b8fab4ee438c76b23d6eebdadf70effa42998c03a279 ./lang/pl.json
37d24f8de343b4796a0a8cfff3a839e55bdb779add0c8119799787d87ad8b953 ./lang/en.json
1610da0b2bbb9424006647adfb95e1318b7670aaed5a2798db1251db82635317 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -84,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
03d5ee12306c95d5ae5df4a2a3a422fe5044e0961bb2be4b7ee49641a9ee7fc1 ./scripts/api_dev_test.py
9083c4a8c602698a53d926864f2fe7e8c9658379f3d82fcb4171f44ec946a7ad ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -104,7 +104,7 @@ b53e1afab342634fedc218ea4517446f19a5ba7985dee599e191b37d9cc8058a ./src/api.rs
27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
f775aa2f3c5c554e4197d47cf381f3d2133ea4c1430a5510f045188dd41e5e51 ./src/api/devices.rs
8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
@@ -169,20 +169,20 @@ fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/c
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs
1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs
53caf9ba4667a2ae33d5c3ce69f85005d9f8540f2ee3c16c2cb3795a53e9d21f ./src/models/runtime.rs
e557ee4a443de586a7ea7dd04318694fb30d6ec053076429503e18f1bcd65c9e ./src/models/runtime.rs
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
5f9f46d3ee6d410b8848ad46f2c482159eebfd692e739b0f09941a43c8d97211 ./src/protocol/gree/binding.rs
2029bad472fdf9f73637be6b7168e9ac79f6bdb55d2bf440d4aa254f3db9659b ./src/protocol/gree/binding.rs
91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs
4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs
034344ab9b86a271854358cfc733d45386c90e8ea66e1f232afd927428422e3a ./src/protocol/gree/discovery.rs
f4e07ca05e037e29d08abb1635f2e6d475522ce517647c422e05b749cd648ad9 ./src/protocol/gree/discovery.rs
ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs
1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs
c438719392dfda8ddf1bccf4ad451038aa4f6aa8dc5477533b0aca96f3661bed ./src/protocol/gree/tests.rs
61d588dc055ac82f0f06fceac5236d5b4b7a0c0d2939144957a174bcbf2fa83c ./src/protocol/gree/tests.rs
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs
b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs
@@ -201,7 +201,7 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree
0b783d6cea599e53fed4ed82fa95e2048c493057c6d57b0dfa423e7cfa10a7ad ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
baff48bb71ba348fe39d52fe8d2bef0614bb0c53c1ff5ee748ce09458b3983b2 ./web/index.html
cc1509fd76f0b0bd4c069db63e1e104210c2bed05f32a3eebac7642f94ef8a99 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js
@@ -213,7 +213,7 @@ b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynam
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js
57d85a638359d720798068ef34c88237ab92ee08dcd7478055b806ff0c858aac ./web/js-dynamic/navigation.js
30cbaebb817d5d416e1a372595b647aae42c14b9cc406c4751d0ced563243b69 ./web/js-dynamic/navigation.js
0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
+8 -6
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.15.6**.
HTTP and WebSocket API for GREE Controller **0.15.7**.
[← Main documentation](../README.md)
@@ -241,7 +241,7 @@ Response:
{
"status": "ok",
"name": "gree-controller",
"version": "0.15.6",
"version": "0.15.7",
"uptime_seconds": 1234,
"control_ready": true,
"time": "2026-08-30T06:54:00Z"
@@ -304,7 +304,7 @@ Returns the initial Web UI snapshot:
"control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
"control_plan_revision": 42,
"system": {
"version": "0.15.6",
"version": "0.15.7",
"uptime_seconds": 1234,
"auth_required": false,
"control_ready": true,
@@ -388,9 +388,9 @@ Scans without adding or binding anything. Request body fields are optional:
}
```
`protocol_version` accepts `0` (auto/both), `1` (AES-ECB only) or `2` (AES-GCM only). Explicit V1/V2 selection rejects replies from the other protocol generation before decryption. The UDP scan packet itself is common to both generations.
`protocol_version` accepts `0` (auto/both), `1` (AES-ECB only) or `2` (AES-GCM only). The UDP scan packet is common to both generations, and newer V2-capable modules may advertise through a legacy/plain discovery envelope. Discovery therefore decodes the reply first and uses the inner `ver` metadata as the protocol hint (`V1.*` -> V1, `V2.*` and newer -> V2). Auto keeps unresolved replies as protocol `0`; explicit V1/V2 scans return only candidates whose hint matches the selected generation.
The response contains candidates with MAC, IP, detected protocol and an `already_added` flag:
The response contains candidates with MAC, IP, protocol hint, a `protocol_locked` flag and an `already_added` flag:
```json
{
@@ -402,6 +402,7 @@ The response contains candidates with MAC, IP, detected protocol and an `already
"ip": "192.168.50.30",
"port": 7000,
"protocol_version": 1,
"protocol_locked": false,
"model": "GREE",
"firmware": "",
"already_added": false
@@ -423,6 +424,7 @@ Add only selected candidates returned by `/api/discovery/scan`:
"ip": "192.168.50.30",
"port": 7000,
"protocol_version": 1,
"protocol_locked": false,
"model": "GREE",
"firmware": "",
"already_added": false
@@ -431,7 +433,7 @@ Add only selected candidates returned by `/api/discovery/scan`:
}
```
Each selected candidate is bound strictly with its detected `protocol_version`. Existing MAC addresses are skipped.
For Auto discovery (`protocol_locked=false`), `protocol_version` is only the preferred bind order. The controller falls back to the other generation if needed and stores the protocol that actually binds successfully. Explicit V1/V2 discovery returns `protocol_locked=true` and binds strictly with the selected protocol. Existing MAC addresses are skipped.
### `GET /api/devices`
+14 -7
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "GREE Controller API",
"version": "0.15.6",
"version": "0.15.7",
"summary": "Local HTTP/WebSocket API for GREE Controller",
"description": "Local API used by the GREE Controller Web UI and Home Assistant integration.",
"license": {
@@ -204,7 +204,7 @@
"Devices"
],
"summary": "Scan for local GREE devices",
"description": "Broadcasts GREE discovery and returns local device candidates without persisting or binding them. protocol_version=1 or 2 strictly accepts only that protocol; protocol_version=0 accepts both.",
"description": "Broadcasts GREE discovery and returns local device candidates without persisting or binding them. The scan envelope is not treated as the device protocol: Auto decodes replies, uses inner ver metadata as a V1/V2 hint and keeps unresolved candidates as protocol 0. Explicit protocol_version=1 or 2 returns only candidates whose resolved hint matches the selected generation.",
"operationId": "scanDiscoveredDevices",
"requestBody": {
"required": true,
@@ -262,7 +262,7 @@
"Devices"
],
"summary": "Add selected discovered devices",
"description": "Persists and binds only the local discovery candidates selected by the caller. Each selected unit is bound strictly with the protocol version detected during the scan.",
"description": "Persists and binds only the local discovery candidates selected by the caller. Auto-discovered candidates use their protocol hint as the preferred bind order, fall back to the other generation, and persist the protocol that actually succeeds. Candidates from explicit V1/V2 discovery are bound strictly.",
"operationId": "addDiscoveredDevices",
"requestBody": {
"required": true,
@@ -281,7 +281,8 @@
"protocol_version": 1,
"model": "GREE",
"firmware": "",
"already_added": false
"already_added": false,
"protocol_locked": false
}
]
}
@@ -5966,7 +5967,7 @@
},
"version": {
"type": "string",
"example": "0.15.6"
"example": "0.15.7"
},
"uptime_seconds": {
"type": "integer",
@@ -6685,7 +6686,7 @@
},
"protocol_version": {
"type": "integer",
"description": "0=auto/both, 1=AES-ECB, 2=AES-GCM",
"description": "0=auto/both with protocol hint + bind verification, 1=AES-ECB only, 2=AES-GCM only",
"minimum": 0,
"maximum": 2,
"example": 0
@@ -9663,10 +9664,16 @@
"protocol_version": {
"type": "integer",
"enum": [
0,
1,
2
],
"description": "Detected protocol generation"
"description": "Discovery protocol hint: 0=unknown/auto, 1=legacy AES-ECB, 2=AES-GCM"
},
"protocol_locked": {
"type": "boolean",
"default": false,
"description": "True when discovery explicitly selected V1 or V2; false for Auto so binding may verify/fallback"
},
"model": {
"type": "string",
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.15.6",
"version": "0.15.7",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
@@ -1,5 +1,12 @@
# Changelog
## 0.15.7
- Fixes mixed V1/V2 Local discovery: legacy/plain discovery envelopes are no longer treated as proof that a unit is V1.
- Uses the inner GREE `ver` metadata as a discovery protocol hint (`V1.*` -> V1, `V2.*`/newer -> V2), while keeping unknown results in Auto mode.
- Auto discovery now verifies the protocol during bind with V1/V2 fallback and persists the generation that actually succeeds; explicit V1/V2 discovery remains locked to the selected protocol.
- The discovery review UI shows Auto for unresolved candidates instead of incorrectly labeling them V1.
## 0.15.6
- Removes the obsolete add-all `POST /api/discovery` endpoint and its compatibility-only merge path.
@@ -1,12 +1,12 @@
name: "GREE Controller"
version: "0.15.6"
version: "0.15.7"
slug: "gree_controller"
description: "Local GREE HVAC controller with Web UI, API, Swagger API docs and Home Assistant integration"
url: "https://repo.example.com/gree-controller-ha-addon/"
description: "Local GREE HVAC controller with Web UI, API, and Home Assistant integration"
url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"
arch:
- amd64
- aarch64
image: "repo.example.com/gree-controller"
image: "zot.linuxiarz.pl/gree-controller"
startup: application
boot: auto
init: false
+1 -1
View File
@@ -1,3 +1,3 @@
name: GREE Controller
url: https://repo.example.com/gree-controller-ha-addon/
url: https://git.linuxiarz.pl/gru/gree-controller-ha-addon/
maintainer: linuxiarz.pl
+2 -2
View File
@@ -296,14 +296,14 @@
"devices.savedAndChecked": "Saved and connection confirmed",
"devices.savedCheckFailed": "Configuration saved, but the connection test failed",
"discovery.title": "Discover GREE devices",
"discovery.help": "Auto accepts both AES-ECB and AES-GCM devices. Selecting V1 or V2 accepts and binds only that protocol. Multiple passes improve discovery when several Wi-Fi modules answer the same broadcast.",
"discovery.help": "Auto accepts both AES-ECB and AES-GCM devices, uses discovery metadata only as a protocol hint and verifies the actual protocol during binding. Selecting V1 or V2 keeps discovery and binding locked to that protocol. Multiple passes improve discovery when several Wi-Fi modules answer the same broadcast.",
"discovery.protocol": "Discovery protocol",
"discovery.passes": "Scan passes",
"discovery.timeout": "Total scan time (ms)",
"zones.quickHint": "Quick target and mode control",
"actions.later": "Later",
"discovery.nameDevices": "Discovered local devices",
"discovery.nameDevicesHelp": "Select the units you want to add. MAC address, IP address and detected protocol are shown for every result.",
"discovery.nameDevicesHelp": "Select the units you want to add. MAC address, IP address and the discovery protocol hint are shown for every result; Auto is resolved during binding.",
"dashboard.thermostats": "Thermostats",
"common.unavailable": "Unavailable",
"mode.off": "Off",
+2 -2
View File
@@ -296,14 +296,14 @@
"devices.savedAndChecked": "Zapisano i potwierdzono połączenie",
"devices.savedCheckFailed": "Zapisano konfigurację, ale test połączenia nie powiódł się",
"discovery.title": "Wykrywanie urządzeń GREE",
"discovery.help": "Auto akceptuje urządzenia AES-ECB i AES-GCM. Wybranie V1 lub V2 powoduje wyszukiwanie i bindowanie wyłącznie tym protokołem. Kilka przebiegów pomaga, gdy wiele modułów Wi-Fi odpowiada na ten sam broadcast.",
"discovery.help": "Auto akceptuje urządzenia AES-ECB i AES-GCM, traktuje dane discovery tylko jako wskazówkę protokołu i potwierdza właściwy protokół podczas bindowania. Wybranie V1 lub V2 blokuje wyszukiwanie i bindowanie do tego protokołu. Kilka przebiegów pomaga, gdy wiele modułów Wi-Fi odpowiada na ten sam broadcast.",
"discovery.protocol": "Protokół wykrywania",
"discovery.passes": "Liczba przebiegów",
"discovery.timeout": "Łączny czas wyszukiwania (ms)",
"zones.quickHint": "Szybka zmiana celu i trybu",
"actions.later": "Później",
"discovery.nameDevices": "Wyszukane urządzenia lokalne",
"discovery.nameDevicesHelp": "Wybierz jednostki, które chcesz dodać. Dla każdego wyniku widoczny jest adres MAC, adres IP i wykryty protokół.",
"discovery.nameDevicesHelp": "Wybierz jednostki, które chcesz dodać. Dla każdego wyniku widoczny jest adres MAC, adres IP i wskazówka protokołu z discovery; Auto zostanie rozstrzygnięte podczas bindowania.",
"dashboard.quickThermostats": "Termostaty",
"common.unavailable": "Niedostępne",
"mode.off": "Wyłączone",
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Developer smoke/integration tests for GREE Controller API 0.15.6.
"""Developer smoke/integration tests for GREE Controller API 0.15.7.
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
@@ -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_TOKEN = os.environ.get("GREE_API_TOKEN", "")
DEFAULT_EXPECTED_VERSION = "0.15.6"
DEFAULT_EXPECTED_VERSION = "0.15.7"
SETTINGS_PATHS: dict[str, set[str]] = {
"/api/settings/application": {"simulator_enabled"},
@@ -593,7 +593,7 @@ def run_suite(args: argparse.Namespace) -> int:
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.15.6 contract", lambda: test_openapi(client, args.expected_version))
runner.run("OpenAPI 0.15.7 contract", lambda: test_openapi(client, args.expected_version))
runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client))
runner.run("protected API requires auth", lambda: test_protected_auth(client))
+30 -10
View File
@@ -2,13 +2,18 @@ fn normalize_local_discovery_mac(value: &str) -> String {
value.replace([':', '-'], "").trim().to_ascii_uppercase()
}
fn local_discovery_candidate(device: &Device, already_added: bool) -> LocalDiscoveryCandidate {
fn local_discovery_candidate(
device: &Device,
already_added: bool,
protocol_locked: bool,
) -> LocalDiscoveryCandidate {
LocalDiscoveryCandidate {
name: device.name.clone(),
mac: device.mac.clone(),
ip: device.ip.clone(),
port: device.port,
protocol_version: device.protocol_version,
protocol_locked,
model: device.model.clone(),
firmware: device.firmware.clone(),
already_added,
@@ -24,11 +29,16 @@ fn device_from_local_discovery(candidate: LocalDiscoveryCandidate) -> Result<Dev
.ip
.parse::<IpAddr>()
.map_err(|_| AppError::BadRequest(format!("invalid IP address for {mac}")))?;
if !matches!(candidate.protocol_version, 1 | 2) {
if candidate.protocol_version > 2 {
return Err(AppError::BadRequest(format!(
"invalid protocol version for {mac}"
)));
}
if candidate.protocol_locked && candidate.protocol_version == 0 {
return Err(AppError::BadRequest(format!(
"locked discovery protocol is missing for {mac}"
)));
}
let model = candidate.model.trim().to_string();
let fallback_model = if model.is_empty() { "GREE" } else { &model };
@@ -151,7 +161,11 @@ async fn scan_discovery(
for device in discovered {
let mac = normalize_local_discovery_mac(&device.mac);
let already_added = state.db.get_device_by_mac(&mac)?.is_some();
candidates.push(local_discovery_candidate(&device, already_added));
candidates.push(local_discovery_candidate(
&device,
already_added,
protocol_version != 0,
));
}
state.log(
@@ -188,6 +202,7 @@ async fn add_discovered_devices(
let mut skipped = Vec::new();
for candidate in request.devices {
let protocol_locked = candidate.protocol_locked;
let mut device = device_from_local_discovery(candidate)?;
let _device_guard = state.lock_device_operation(&device.id).await;
if state.db.get_device_by_mac(&device.mac)?.is_some() {
@@ -195,13 +210,17 @@ async fn add_discovered_devices(
continue;
}
match state
.providers
.local()
.client()
.bind_exact(&device, device.protocol_version)
.await
{
let client = state.providers.local().client();
let bind_result = if protocol_locked {
client.bind_exact(&device, device.protocol_version).await
} else {
// Auto discovery only provides a protocol hint. Try that generation
// first, fall back to the other one, and persist the protocol that
// actually completes binding.
client.bind(&device).await
};
match bind_result {
Ok(bound) => {
device.key = Some(bound.key);
device.protocol_version = bound.protocol_version;
@@ -217,6 +236,7 @@ async fn add_discovered_devices(
json!({
"device_id": device.id,
"protocol_version": device.protocol_version,
"protocol_locked": protocol_locked,
}),
);
}
+6 -1
View File
@@ -124,8 +124,13 @@ pub struct LocalDiscoveryCandidate {
pub ip: String,
#[serde(default = "default_port")]
pub port: u16,
/// Detected protocol: 1 = AES-ECB, 2 = AES-GCM.
/// Discovery protocol hint: 0 = unknown/auto, 1 = AES-ECB, 2 = AES-GCM.
pub protocol_version: u8,
/// True only when the discovery request explicitly selected V1 or V2.
/// Auto discovery keeps this false so binding can verify/fallback and store
/// the protocol that actually succeeds.
#[serde(default)]
pub protocol_locked: bool,
#[serde(default)]
pub model: String,
#[serde(default)]
+1 -1
View File
@@ -4,7 +4,7 @@ impl GreeClient {
self.bind_with_versions(device, versions).await
}
/// Bind using exactly the protocol detected during discovery.
/// Bind using exactly the requested protocol. Used by explicit V1/V2 discovery.
pub async fn bind_exact(&self, device: &Device, protocol_version: u8) -> Result<BindResult> {
let versions = Self::exact_bind_versions(protocol_version)?;
self.bind_with_versions(device, versions).await
+38 -13
View File
@@ -74,40 +74,65 @@ impl GreeClient {
Ok(result)
}
fn discovery_protocol_hint(value: &Value, envelope_protocol: u8) -> u8 {
// A GCM envelope proves V2 support. A V1/plain discovery envelope does
// not prove V1 because newer modules also advertise themselves that way.
if envelope_protocol == 2 {
return 2;
}
let ver = value
.get("ver")
.and_then(Value::as_str)
.unwrap_or_default()
.trim();
let ver = ver
.strip_prefix('V')
.or_else(|| ver.strip_prefix('v'))
.unwrap_or(ver);
let major = ver
.split('.')
.next()
.and_then(|part| part.parse::<u16>().ok());
match major {
Some(1) => 1,
Some(value) if value >= 2 => 2,
_ => 0,
}
}
fn parse_discovery(
&self,
mut value: Value,
source: SocketAddr,
protocol_filter: u8,
) -> Result<Option<Device>> {
let mut detected_protocol = 1_u8;
// Discovery transport is not the same thing as the protocol used for
// bind/status/commands. In particular, AES-GCM capable modules can
// answer the common scan packet using a legacy/plain V1-style envelope.
// Keep the envelope only as a hint and resolve the device generation
// from the inner discovery payload after it has been decoded.
let mut envelope_protocol = 0_u8;
if value.get("t").and_then(Value::as_str) == Some("pack") {
if let Some(pack_value) = value.get("pack") {
if let Some(pack) = pack_value.as_str() {
let packet_protocol = if value.get("tag").and_then(Value::as_str).is_some() {
2
} else {
1
};
if protocol_filter != 0 && packet_protocol != protocol_filter {
return Ok(None);
}
let clear = if let Some(tag) = value.get("tag").and_then(Value::as_str) {
detected_protocol = 2;
envelope_protocol = 2;
decrypt_v2(GENERIC_GREE_V2_KEY, pack, tag)?
} else {
envelope_protocol = 1;
decrypt_v1(GENERIC_GREE_V1_KEY, pack)?
};
value = serde_json::from_slice::<Value>(&clear)
.context("invalid decrypted discovery JSON")?;
} else if pack_value.is_object() {
if protocol_filter == 2 {
return Ok(None);
}
value = pack_value.clone();
}
}
}
let detected_protocol = Self::discovery_protocol_hint(&value, envelope_protocol);
if protocol_filter != 0 && detected_protocol != protocol_filter {
return Ok(None);
}
+65 -7
View File
@@ -120,7 +120,7 @@ mod tests {
}
#[test]
fn discovery_bind_uses_detected_protocol_exactly() {
fn bind_version_selection_supports_exact_and_auto_fallback() {
assert_eq!(GreeClient::exact_bind_versions(1).unwrap(), &[1]);
assert_eq!(GreeClient::exact_bind_versions(2).unwrap(), &[2]);
assert!(GreeClient::exact_bind_versions(0).is_err());
@@ -130,7 +130,7 @@ mod tests {
}
#[test]
fn discovery_protocol_filter_rejects_other_envelope_before_decode() {
fn discovery_uses_inner_version_as_protocol_hint() {
let client = GreeClient::new(
"test-controller".into(),
None,
@@ -138,20 +138,78 @@ mod tests {
Arc::new(AtomicBool::new(false)),
);
let source: SocketAddr = "192.0.2.20:7000".parse().unwrap();
let v1 = json!({
"t": "pack",
"pack": {
"t": "dev",
"mac": "502CC699C117",
"name": "Legacy",
"ver": "V1.2.1"
}
});
let v2_inner = json!({
"t": "dev",
"mac": "9424B80C5AB9",
"name": "Modern",
"ver": "V3.4"
});
let v2 = json!({
"t": "pack",
// Reproduce the mixed-generation case: the device is V2-capable,
// but its discovery payload is carried in the legacy V1 envelope.
"pack": encrypt_v1(
GENERIC_GREE_V1_KEY,
&serde_json::to_vec(&v2_inner).unwrap(),
)
.unwrap()
});
let legacy = client
.parse_discovery(v1.clone(), source, 0)
.unwrap()
.expect("V1 discovery");
let modern = client
.parse_discovery(v2.clone(), source, 0)
.unwrap()
.expect("V2 discovery");
assert_eq!(legacy.protocol_version, 1);
assert_eq!(modern.protocol_version, 2);
assert!(client.parse_discovery(v1.clone(), source, 1).unwrap().is_some());
assert!(client.parse_discovery(v1, source, 2).unwrap().is_none());
assert!(client.parse_discovery(v2.clone(), source, 2).unwrap().is_some());
assert!(client.parse_discovery(v2, source, 1).unwrap().is_none());
}
#[test]
fn discovery_does_not_treat_plain_or_v1_envelope_as_definitive_v1() {
let client = GreeClient::new(
"test-controller".into(),
None,
None,
Arc::new(AtomicBool::new(false)),
);
let source: SocketAddr = "192.0.2.21:7000".parse().unwrap();
let unknown = json!({
"t": "pack",
"pack": {
"t": "dev",
"mac": "AABBCCDDEEFF",
"name": "Test"
"name": "Unknown"
}
});
assert!(client
.parse_discovery(v1.clone(), source, 1)
let candidate = client
.parse_discovery(unknown.clone(), source, 0)
.unwrap()
.is_some());
assert!(client.parse_discovery(v1, source, 2).unwrap().is_none());
.expect("auto discovery must retain unknown protocol");
assert_eq!(candidate.protocol_version, 0);
assert!(client
.parse_discovery(unknown.clone(), source, 1)
.unwrap()
.is_none());
assert!(client.parse_discovery(unknown, source, 2).unwrap().is_none());
}
}
+2 -2
View File
@@ -1263,7 +1263,7 @@
<h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close
data-ui-icon="close"></button>
</div>
<p class="field-note" data-i18n="discovery.help">Auto accepts both AES-ECB and AES-GCM devices. Selecting V1 or V2 accepts and binds only that protocol. Multiple passes improve discovery when several Wi-Fi modules answer the same broadcast.</p>
<p class="field-note" data-i18n="discovery.help">Auto accepts both AES-ECB and AES-GCM devices, uses discovery metadata only as a protocol hint and verifies the actual protocol during binding. Selecting V1 or V2 keeps discovery and binding locked to that protocol. Multiple passes improve discovery when several Wi-Fi modules answer the same broadcast.</p>
<label><span data-i18n="discovery.protocol">Discovery protocol</span><select name="protocol_version">
<option value="0" data-i18n="devices.protocolAuto">Auto (V1 + V2)</option>
<option value="1">V1 AES-ECB</option>
@@ -1284,7 +1284,7 @@
<h2 data-i18n="discovery.nameDevices">Discovered local devices</h2><button type="button" data-close
data-ui-icon="close"></button>
</div>
<p class="field-note" data-i18n="discovery.nameDevicesHelp">Select the units you want to add. MAC address, IP address and detected protocol are shown for every result.</p>
<p class="field-note" data-i18n="discovery.nameDevicesHelp">Select the units you want to add. MAC address, IP address and the discovery protocol hint are shown for every result; Auto is resolved during binding.</p>
<div id="discoveryNamesList" class="discovery-name-list"></div>
<div class="form-actions"><button type="button" class="secondary" data-close
data-i18n="actions.cancel">Cancel</button><button class="primary" type="submit"
+2 -1
View File
@@ -476,7 +476,8 @@ function showDiscoveryNames(devices) {
if (!list) return;
list.innerHTML = items.length ? items.map((device, index) => {
const alreadyAdded = device.already_added === true;
const protocol = Number(device.protocol_version) === 2 ? 'V2 AES-GCM' : 'V1 AES-ECB';
const protocolVersion = Number(device.protocol_version);
const protocol = protocolVersion === 2 ? 'V2 AES-GCM' : protocolVersion === 1 ? 'V1 AES-ECB' : tr('devices.protocolAuto');
const selectorId = `local-discovery-${index}`;
return `<div class="discovery-name-row local-discovery-row">
<div class="local-discovery-head">