diff --git a/Cargo.lock b/Cargo.lock index e8e7ddc..9bb284e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.15.6" +version = "0.15.7" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 29b3c59..8b7b4f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index ff1c372..18af4e9 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -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 diff --git a/README.md b/README.md index 018ecd8..f123a91 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index fe6042d..8b738f4 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -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 diff --git a/docs/API.md b/docs/API.md index 086ee58..89732ee 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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` diff --git a/docs/openapi.json b/docs/openapi.json index c6a7db6..79617c3 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -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", diff --git a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json index 41400aa..be3fca1 100644 --- a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json +++ b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json @@ -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", diff --git a/ha-addon/repository/gree-controller/CHANGELOG.md b/ha-addon/repository/gree-controller/CHANGELOG.md index 669af69..8ba1998 100644 --- a/ha-addon/repository/gree-controller/CHANGELOG.md +++ b/ha-addon/repository/gree-controller/CHANGELOG.md @@ -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. diff --git a/ha-addon/repository/gree-controller/config.yaml b/ha-addon/repository/gree-controller/config.yaml index 837dae0..400c777 100644 --- a/ha-addon/repository/gree-controller/config.yaml +++ b/ha-addon/repository/gree-controller/config.yaml @@ -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 diff --git a/ha-addon/repository/repository.yaml b/ha-addon/repository/repository.yaml index ac765b2..c5b53d9 100644 --- a/ha-addon/repository/repository.yaml +++ b/ha-addon/repository/repository.yaml @@ -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 diff --git a/lang/en.json b/lang/en.json index a238b3e..c737a64 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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", diff --git a/lang/pl.json b/lang/pl.json index 351ac29..0087068 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -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", diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py index d924165..fe9e9b7 100644 --- a/scripts/api_dev_test.py +++ b/scripts/api_dev_test.py @@ -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)) diff --git a/src/api/devices.rs b/src/api/devices.rs index 2b13ba5..69d1c89 100644 --- a/src/api/devices.rs +++ b/src/api/devices.rs @@ -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() .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, }), ); } diff --git a/src/models/runtime.rs b/src/models/runtime.rs index ad9d4b6..e7b9bed 100644 --- a/src/models/runtime.rs +++ b/src/models/runtime.rs @@ -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)] diff --git a/src/protocol/gree/binding.rs b/src/protocol/gree/binding.rs index dd30076..90a0dfc 100644 --- a/src/protocol/gree/binding.rs +++ b/src/protocol/gree/binding.rs @@ -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 { let versions = Self::exact_bind_versions(protocol_version)?; self.bind_with_versions(device, versions).await diff --git a/src/protocol/gree/discovery.rs b/src/protocol/gree/discovery.rs index 179a64d..707cc5c 100644 --- a/src/protocol/gree/discovery.rs +++ b/src/protocol/gree/discovery.rs @@ -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::().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> { - 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::(&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); } diff --git a/src/protocol/gree/tests.rs b/src/protocol/gree/tests.rs index b93ca3e..1016adf 100644 --- a/src/protocol/gree/tests.rs +++ b/src/protocol/gree/tests.rs @@ -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()); } } diff --git a/web/index.html b/web/index.html index a4863c1..b0dbefd 100644 --- a/web/index.html +++ b/web/index.html @@ -1263,7 +1263,7 @@

Discover GREE devices

-

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.

+

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.