diff --git a/Cargo.lock b/Cargo.lock index 55f0d7d..e8e7ddc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.15.4" +version = "0.15.6" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 1002ee0..29b3c59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.15.4" +version = "0.15.6" 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 bdd694c..ff1c372 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,13 +1,14 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -37e255dd094e4720f96b74e501a179cf81c838ff327cbc1924b8fc4b5ea555fa ./Cargo.toml +03fd19b33f6b87a3cf55957c44c619dc65b6c1ef6ab35a275f7142e063fa130d ./Cargo.lock +0f1ebc69bb39b480c603772736bbc968cdeada29efecca6a056e4ebd0b071cb7 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -c1cac4ca532e73d64e5b090ad410a2161e9790b8d77fc6732adf52c248fb3472 ./README.md -8dc1f42dd20c98873fcd0a979475560f9ec79e098f01375de47f8fd96ef6e80d ./SOURCE_MANIFEST.sha256 +252802f9b072e316a1898b5b39922788c0ed7ab769aba6e1cb33e5ac5595d7fb ./README.md +fb12007189dad1c860aeed1eacc561c54afb01204396982b42bfa68706dc583e ./SOURCE_MANIFEST.sha256 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -0da3db314314e19a00c08700f94495075fecd1e54a5b188f6a62cb237307e96f ./docs/API.md +c52fb51807e46086e6bb44dd532dde346a742d12d52ce703055f1d4f433fd253 ./docs/API.md a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md -55ffdcc5b0a43d930823cea7c94235990dbbc4a35b60832b9adca1faf606d036 ./docs/openapi.json +524977fb867f2b56b56df827df0b8f646e366c00fa31d62c577a5a118f76406f ./docs/openapi.json 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md @@ -21,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 -d1ec103c9a167382e33877183b6df221a5cced5db58cf5d6eefd2eb24ab16077 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +45af41dd19b03ddc1d786730603ff2067bf3b3a794b7663acea8e45b5c1b7200 ./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 +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 -1d1508e247b67487e6aac52b532adec2842494ec2e60144491c5144967a4f8f3 ./ha-addon/repository/gree-controller/CHANGELOG.md +732265515c1d45eb11b966919cfa29ed9f600b6dd02a05e1abee8afda4151213 ./ha-addon/repository/gree-controller/CHANGELOG.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md -d060ee39cdea4de68bd9ededaebb7b81d974821e418b69e00c39cbdebd61f801 ./ha-addon/repository/gree-controller/config.yaml +d742c16e5f5f24be48f8055c2131ff335fb5072469d7d515328fd41e9c3aca34 ./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 -2153b230357aab4766398577e23a7a9ff83bb135ddc86d1b3f2cbfc852efe0a1 ./ha-addon/repository/repository.yaml +b54c60ceeac06d32af7797da9e83b90cb037600318e00512da523d37b94d3081 ./ha-addon/repository/repository.yaml cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run.sh 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md -e5c6a6051381e28d74b75e48232de13d02eb87519d29c847b7ab7979ce47d082 ./lang/en.json -02c650f603bca1bd17d9d59193c176ee6ddccb8304f2e658d7ba7730f8ff5f83 ./lang/pl.json +2175f4ce8b5d994e1e4f912244b03721a93a41e2970f39826d6ea15fdaa7078e ./lang/en.json +1055d5835329c03eb6f5b8fab4ee438c76b23d6eebdadf70effa42998c03a279 ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -84,7 +85,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md -b9bd0f1db6bf879ca3778c4969dd56d3c35b7bdd137df517dbd2cd2e11856080 ./scripts/api_dev_test.py +03d5ee12306c95d5ae5df4a2a3a422fe5044e0961bb2be4b7ee49641a9ee7fc1 ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -97,14 +98,14 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh -f4ace9a1104ab1d0016db1d34c3ac2c4010fc1a5534af5fbf02d2042d5815d8c ./src/api.rs +b53e1afab342634fedc218ea4517446f19a5ba7985dee599e191b37d9cc8058a ./src/api.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs 9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs 27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs -5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs +f775aa2f3c5c554e4197d47cf381f3d2133ea4c1430a5510f045188dd41e5e51 ./src/api/devices.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs 2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs @@ -113,7 +114,7 @@ b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/hist e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs 8495967c3df72110f591c744975984c78cafc8181b2d430e7fd5b9baf57eba95 ./src/api/integrations.rs 7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs -c01b8b942e64a46dac19ce08346aa844df7e9ac7147c0ad6d0f2cfd90e071ec0 ./src/api/openapi.rs +9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs 5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs @@ -169,25 +170,24 @@ fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/c aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs 6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs 1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs -f1359b68f1d041cd441f80429e77893eeb6f6194613b58fd3e7a212fb511db3e ./src/models/runtime.rs +53caf9ba4667a2ae33d5c3ce69f85005d9f8540f2ee3c16c2cb3795a53e9d21f ./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 -daca14e9a4dd65394add7c7753c8ae5e7e43e991323a5ca23d8788aae4428925 ./src/protocol/gree.rs -7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs +578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs +5f9f46d3ee6d410b8848ad46f2c482159eebfd692e739b0f09941a43c8d97211 ./src/protocol/gree/binding.rs 91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs 4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs -668e3bfcc4be84d1a86909bfc18e0d3816e0343cdc0d9b43189f7b0c6e0666d4 ./src/protocol/gree/discovery.rs -0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs +034344ab9b86a271854358cfc733d45386c90e8ea66e1f232afd927428422e3a ./src/protocol/gree/discovery.rs ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs 1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs -e0555fac67d1dd0129ecfea6d1fd2fbeb1a73ac89ad95d1b74a7094e50e29ae4 ./src/protocol/gree/tests.rs +c438719392dfda8ddf1bccf4ad451038aa4f6aa8dc5477533b0aca96f3661bed ./src/protocol/gree/tests.rs 3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs -6258ee3888a5f9a4f8bb6cfbbac97f2621de07f9da6bcb2fa596d452f47f2e49 ./src/protocol/mod.rs +5f70211b73215d9ae7af8fc306d15dc0b6cfbf0df9f79e34f1c785957fdf8bd4 ./src/protocol/mod.rs a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider.rs 830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs 7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs @@ -199,22 +199,22 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/ 8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html -2822a98a4d7f72f2b89b70878aa0f9bd3f391fde4ea04ba1b94e89a527dcd1fb ./web/css/styles.css +0b783d6cea599e53fed4ed82fa95e2048c493057c6d57b0dfa423e7cfa10a7ad ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -d5115a9dc59b4e753dcff9239a77fe49886951d7a4ae7456be80d9201cc42365 ./web/index.html +baff48bb71ba348fe39d52fe8d2bef0614bb0c53c1ff5ee748ce09458b3983b2 ./web/index.html 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js -b5fd43eeb9c15fe8289b9e718dfbc2651417379462c4b16b88e090b436abf7c5 ./web/js-dynamic/core.js +81012bd88282034cdc4e876af5eb5057289f4db8505847568bc3c3701a348d4b ./web/js-dynamic/core.js 752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js 6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js -a48cb69ae4a2d0d6d512baef346fe499a5a6ea267e071856829df982d4a0f129 ./web/js-dynamic/events.js +b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js 8e0f294ceeec1d258edb56271469a8a05381e6443a475f76fbeded6637d17503 ./web/js-dynamic/flows.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js -c974b6ef41ce8b6def35f5349849a06855c46ec31cb4122a39fcb5c78f6d362a ./web/js-dynamic/navigation.js +57d85a638359d720798068ef34c88237ab92ee08dcd7478055b806ff0c858aac ./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 58a647e..018ecd8 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.4** +**Version: 0.15.6** -Version 0.15.4 refreshes the Rust dependency stack, including Axum 0.8, Reqwest 0.13, Rusqlite 0.40, Rand 0.10, RustCrypto AES/AES-GCM and utoipa-swagger-ui 9, with the required compatibility migrations. +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. > [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,8 +185,8 @@ 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 is recommended; the controller accepts both supported GREE encryption generations. -4. Newly discovered devices are bound automatically when possible. Manual bind is also available. +3. Auto protocol mode accepts both supported GREE encryption generations. Selecting V1 or V2 strictly filters discovery and bind 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. Discovery uses UDP broadcast, so routed/VLAN networks must explicitly permit or relay the required traffic. diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index 0f4947c..fe6042d 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -1,12 +1,13 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -37e255dd094e4720f96b74e501a179cf81c838ff327cbc1924b8fc4b5ea555fa ./Cargo.toml +03fd19b33f6b87a3cf55957c44c619dc65b6c1ef6ab35a275f7142e063fa130d ./Cargo.lock +0f1ebc69bb39b480c603772736bbc968cdeada29efecca6a056e4ebd0b071cb7 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -c1cac4ca532e73d64e5b090ad410a2161e9790b8d77fc6732adf52c248fb3472 ./README.md +252802f9b072e316a1898b5b39922788c0ed7ab769aba6e1cb33e5ac5595d7fb ./README.md 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -0da3db314314e19a00c08700f94495075fecd1e54a5b188f6a62cb237307e96f ./docs/API.md +c52fb51807e46086e6bb44dd532dde346a742d12d52ce703055f1d4f433fd253 ./docs/API.md a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md -55ffdcc5b0a43d930823cea7c94235990dbbc4a35b60832b9adca1faf606d036 ./docs/openapi.json +524977fb867f2b56b56df827df0b8f646e366c00fa31d62c577a5a118f76406f ./docs/openapi.json 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md @@ -20,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 -d1ec103c9a167382e33877183b6df221a5cced5db58cf5d6eefd2eb24ab16077 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +45af41dd19b03ddc1d786730603ff2067bf3b3a794b7663acea8e45b5c1b7200 ./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 @@ -28,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 -1d1508e247b67487e6aac52b532adec2842494ec2e60144491c5144967a4f8f3 ./ha-addon/repository/gree-controller/CHANGELOG.md +732265515c1d45eb11b966919cfa29ed9f600b6dd02a05e1abee8afda4151213 ./ha-addon/repository/gree-controller/CHANGELOG.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md -d060ee39cdea4de68bd9ededaebb7b81d974821e418b69e00c39cbdebd61f801 ./ha-addon/repository/gree-controller/config.yaml +d742c16e5f5f24be48f8055c2131ff335fb5072469d7d515328fd41e9c3aca34 ./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 -2153b230357aab4766398577e23a7a9ff83bb135ddc86d1b3f2cbfc852efe0a1 ./ha-addon/repository/repository.yaml +b54c60ceeac06d32af7797da9e83b90cb037600318e00512da523d37b94d3081 ./ha-addon/repository/repository.yaml cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run.sh 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md -e5c6a6051381e28d74b75e48232de13d02eb87519d29c847b7ab7979ce47d082 ./lang/en.json -02c650f603bca1bd17d9d59193c176ee6ddccb8304f2e658d7ba7730f8ff5f83 ./lang/pl.json +2175f4ce8b5d994e1e4f912244b03721a93a41e2970f39826d6ea15fdaa7078e ./lang/en.json +1055d5835329c03eb6f5b8fab4ee438c76b23d6eebdadf70effa42998c03a279 ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -83,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md -b9bd0f1db6bf879ca3778c4969dd56d3c35b7bdd137df517dbd2cd2e11856080 ./scripts/api_dev_test.py +03d5ee12306c95d5ae5df4a2a3a422fe5044e0961bb2be4b7ee49641a9ee7fc1 ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -96,14 +97,14 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh -f4ace9a1104ab1d0016db1d34c3ac2c4010fc1a5534af5fbf02d2042d5815d8c ./src/api.rs +b53e1afab342634fedc218ea4517446f19a5ba7985dee599e191b37d9cc8058a ./src/api.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs 9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs 27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs -5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs +f775aa2f3c5c554e4197d47cf381f3d2133ea4c1430a5510f045188dd41e5e51 ./src/api/devices.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs 2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs @@ -112,7 +113,7 @@ b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/hist e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs 8495967c3df72110f591c744975984c78cafc8181b2d430e7fd5b9baf57eba95 ./src/api/integrations.rs 7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs -c01b8b942e64a46dac19ce08346aa844df7e9ac7147c0ad6d0f2cfd90e071ec0 ./src/api/openapi.rs +9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs 5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs @@ -168,25 +169,24 @@ fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/c aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs 6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs 1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs -f1359b68f1d041cd441f80429e77893eeb6f6194613b58fd3e7a212fb511db3e ./src/models/runtime.rs +53caf9ba4667a2ae33d5c3ce69f85005d9f8540f2ee3c16c2cb3795a53e9d21f ./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 -daca14e9a4dd65394add7c7753c8ae5e7e43e991323a5ca23d8788aae4428925 ./src/protocol/gree.rs -7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs +578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs +5f9f46d3ee6d410b8848ad46f2c482159eebfd692e739b0f09941a43c8d97211 ./src/protocol/gree/binding.rs 91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs 4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs -668e3bfcc4be84d1a86909bfc18e0d3816e0343cdc0d9b43189f7b0c6e0666d4 ./src/protocol/gree/discovery.rs -0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs +034344ab9b86a271854358cfc733d45386c90e8ea66e1f232afd927428422e3a ./src/protocol/gree/discovery.rs ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs 1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs -e0555fac67d1dd0129ecfea6d1fd2fbeb1a73ac89ad95d1b74a7094e50e29ae4 ./src/protocol/gree/tests.rs +c438719392dfda8ddf1bccf4ad451038aa4f6aa8dc5477533b0aca96f3661bed ./src/protocol/gree/tests.rs 3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs -6258ee3888a5f9a4f8bb6cfbbac97f2621de07f9da6bcb2fa596d452f47f2e49 ./src/protocol/mod.rs +5f70211b73215d9ae7af8fc306d15dc0b6cfbf0df9f79e34f1c785957fdf8bd4 ./src/protocol/mod.rs a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider.rs 830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs 7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs @@ -198,22 +198,22 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/ 8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html -2822a98a4d7f72f2b89b70878aa0f9bd3f391fde4ea04ba1b94e89a527dcd1fb ./web/css/styles.css +0b783d6cea599e53fed4ed82fa95e2048c493057c6d57b0dfa423e7cfa10a7ad ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -d5115a9dc59b4e753dcff9239a77fe49886951d7a4ae7456be80d9201cc42365 ./web/index.html +baff48bb71ba348fe39d52fe8d2bef0614bb0c53c1ff5ee748ce09458b3983b2 ./web/index.html 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js -b5fd43eeb9c15fe8289b9e718dfbc2651417379462c4b16b88e090b436abf7c5 ./web/js-dynamic/core.js +81012bd88282034cdc4e876af5eb5057289f4db8505847568bc3c3701a348d4b ./web/js-dynamic/core.js 752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js 6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.js -a48cb69ae4a2d0d6d512baef346fe499a5a6ea267e071856829df982d4a0f129 ./web/js-dynamic/events.js +b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js 8e0f294ceeec1d258edb56271469a8a05381e6443a475f76fbeded6637d17503 ./web/js-dynamic/flows.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js -c974b6ef41ce8b6def35f5349849a06855c46ec31cb4122a39fcb5c78f6d362a ./web/js-dynamic/navigation.js +57d85a638359d720798068ef34c88237ab92ee08dcd7478055b806ff0c858aac ./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 e5fa901..086ee58 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.4**. +HTTP and WebSocket API for GREE Controller **0.15.6**. [← Main documentation](../README.md) @@ -132,7 +132,8 @@ Common statuses: | Method | Endpoint | Description | | --- | --- | --- | -| POST | `/api/discovery` | Discover/bind GREE devices. | +| POST | `/api/discovery/scan` | Scan for local GREE devices without adding them. | +| POST | `/api/discovery/add` | Add and bind selected scan results. | | GET | `/api/devices` | List devices. | | POST | `/api/devices` | Add a device manually. | | GET | `/api/devices/{id}` | Read a device. | @@ -240,7 +241,7 @@ Response: { "status": "ok", "name": "gree-controller", - "version": "0.15.4", + "version": "0.15.6", "uptime_seconds": 1234, "control_ready": true, "time": "2026-08-30T06:54:00Z" @@ -303,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.4", + "version": "0.15.6", "uptime_seconds": 1234, "auth_required": false, "control_ready": true, @@ -374,9 +375,9 @@ A device response contains: | `communication_failures` | integer | Consecutive/recorded communication failure counter. | | `created_at`, `updated_at` | ISO-8601 | Resource timestamps. | -### `POST /api/discovery` +### `POST /api/discovery/scan` -Request body, all fields optional: +Scans without adding or binding anything. Request body fields are optional: ```json { @@ -387,23 +388,51 @@ Request body, all fields optional: } ``` -Rules: +`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. -- `timeout_ms`: effective range `500..30000` ms. -- `protocol_version`: `0` auto/both, `1` AES-ECB only, `2` AES-GCM only. -- `passes`: `1..10`. -- Missing values use runtime GREE settings. - -Successful discovery merges known devices, tries binding devices that do not have a key, persists results and returns: +The response contains candidates with MAC, IP, detected protocol and an `already_added` flag: ```json { "count": 1, - "devices": [], - "new_device_ids": ["gree-aabbccddeeff"] + "devices": [ + { + "name": "GREE EEFF", + "mac": "AABBCCDDEEFF", + "ip": "192.168.50.30", + "port": 7000, + "protocol_version": 1, + "model": "GREE", + "firmware": "", + "already_added": false + } + ] } ``` +### `POST /api/discovery/add` + +Add only selected candidates returned by `/api/discovery/scan`: + +```json +{ + "devices": [ + { + "name": "Living room", + "mac": "AABBCCDDEEFF", + "ip": "192.168.50.30", + "port": 7000, + "protocol_version": 1, + "model": "GREE", + "firmware": "", + "already_added": false + } + ] +} +``` + +Each selected candidate is bound strictly with its detected `protocol_version`. Existing MAC addresses are skipped. + ### `GET /api/devices` Returns `Device[]`. @@ -1599,10 +1628,10 @@ BASE='http://127.0.0.1:8787' AUTH='Authorization: Bearer APP_TOKEN' ``` -Discover devices: +Scan local devices: ```bash -curl -X POST "$BASE/api/discovery" -H "$AUTH" -H 'Content-Type: application/json' \ +curl -X POST "$BASE/api/discovery/scan" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"protocol_version":0,"passes":3}' ``` diff --git a/docs/openapi.json b/docs/openapi.json index 48de255..c6a7db6 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.4", + "version": "0.15.6", "summary": "Local HTTP/WebSocket API for GREE Controller", "description": "Local API used by the GREE Controller Web UI and Home Assistant integration.", "license": { @@ -198,14 +198,14 @@ } } }, - "/api/discovery": { + "/api/discovery/scan": { "post": { "tags": [ "Devices" ], - "summary": "Discover GREE devices", - "description": "Broadcasts GREE discovery, merges known devices, attempts binding for devices without a key, persists results and reports newly discovered controller IDs.", - "operationId": "discoverDevices", + "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.", + "operationId": "scanDiscoveredDevices", "requestBody": { "required": true, "content": { @@ -232,11 +232,77 @@ ], "responses": { "200": { - "description": "Discovery result", + "description": "Discovery candidates; no devices are added automatically", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DiscoveryResponse" + "$ref": "#/components/schemas/DiscoveryScanResponse" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "502": { + "$ref": "#/components/responses/BadGateway" + }, + "500": { + "$ref": "#/components/responses/InternalError" + } + } + } + }, + "/api/discovery/add": { + "post": { + "tags": [ + "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.", + "operationId": "addDiscoveredDevices", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddDiscoveredDevicesRequest" + }, + "example": { + "devices": [ + { + "name": "Living room", + "mac": "AABBCCDDEEFF", + "ip": "192.168.50.30", + "port": 7000, + "protocol_version": 1, + "model": "GREE", + "firmware": "", + "already_added": false + } + ] + } + } + } + }, + "security": [ + { + "BearerToken": [] + }, + { + "ApiTokenHeader": [] + } + ], + "responses": { + "200": { + "description": "Selected devices added", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddDiscoveredDevicesResponse" } } } @@ -5900,7 +5966,7 @@ }, "version": { "type": "string", - "example": "0.15.4" + "example": "0.15.6" }, "uptime_seconds": { "type": "integer", @@ -6633,37 +6699,6 @@ } } }, - "DiscoveryResponse": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "minimum": 0, - "example": 1 - }, - "devices": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Device" - } - }, - "new_device_ids": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "gree-aabbccddeeff" - ] - } - }, - "required": [ - "count", - "devices", - "new_device_ids" - ], - "additionalProperties": false - }, "TemporaryQuickThermostatRequest": { "type": "object", "description": "Persisted temporary thermostat session with delayed/absolute start and several finish rules.", @@ -9602,6 +9637,127 @@ "source" ], "additionalProperties": false + }, + "LocalDiscoveryCandidate": { + "type": "object", + "description": "Local GREE unit returned by discovery. No device is persisted until it is submitted to /api/discovery/add.", + "properties": { + "name": { + "type": "string", + "example": "Living room" + }, + "mac": { + "type": "string", + "example": "AABBCCDDEEFF" + }, + "ip": { + "type": "string", + "example": "192.168.50.30" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "default": 7000 + }, + "protocol_version": { + "type": "integer", + "enum": [ + 1, + 2 + ], + "description": "Detected protocol generation" + }, + "model": { + "type": "string", + "example": "GREE" + }, + "firmware": { + "type": "string", + "example": "1.0" + }, + "already_added": { + "type": "boolean", + "description": "True when this MAC already exists in the controller" + } + }, + "required": [ + "name", + "mac", + "ip", + "port", + "protocol_version", + "model", + "firmware", + "already_added" + ], + "additionalProperties": false + }, + "AddDiscoveredDevicesRequest": { + "type": "object", + "properties": { + "devices": { + "type": "array", + "minItems": 1, + "maxItems": 64, + "items": { + "$ref": "#/components/schemas/LocalDiscoveryCandidate" + } + } + }, + "required": [ + "devices" + ], + "additionalProperties": false + }, + "AddDiscoveredDevicesResponse": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0 + }, + "devices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Device" + } + }, + "skipped_macs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "MAC addresses skipped because they were already registered" + } + }, + "required": [ + "count", + "devices", + "skipped_macs" + ], + "additionalProperties": false + }, + "DiscoveryScanResponse": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0, + "example": 1 + }, + "devices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocalDiscoveryCandidate" + } + } + }, + "required": [ + "count", + "devices" + ], + "additionalProperties": false } }, "responses": { 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 466a331..41400aa 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.4", + "version": "0.15.6", "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 715baa6..669af69 100644 --- a/ha-addon/repository/gree-controller/CHANGELOG.md +++ b/ha-addon/repository/gree-controller/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.15.6 + +- Removes the obsolete add-all `POST /api/discovery` endpoint and its compatibility-only merge path. +- Local discovery now has one flow only: `POST /api/discovery/scan` followed by `POST /api/discovery/add`. +- Simplifies selected-device binding: `/api/discovery/add` no longer accepts a redundant top-level `protocol_version`; each unit is bound strictly with the protocol detected during scan. +- Removes unused discovery merge/export code and the matching obsolete OpenAPI schema. + +## 0.15.5 + +- Local discovery no longer adds every detected unit automatically; scan results are reviewed first and only selected units are persisted and bound. +- Discovery results show each unit model, MAC address, IP address and detected protocol, with already-added units clearly marked. +- Explicit V1 AES-ECB or V2 AES-GCM discovery now strictly filters the other protocol and binds only with the selected protocol; Auto keeps V1/V2 fallback. +- Adds `POST /api/discovery/scan` and `POST /api/discovery/add` for the selective local-discovery flow. + ## 0.15.4 - Updates the Rust dependency stack to current major/minor releases, including Axum 0.8, Reqwest 0.13, Rusqlite 0.40, Rand 0.10, AES 0.9, AES-GCM 0.11, Base64 0.23, SHA-2 0.11, Tower HTTP 0.7 and utoipa-swagger-ui 9. diff --git a/ha-addon/repository/gree-controller/config.yaml b/ha-addon/repository/gree-controller/config.yaml index 047f1d8..837dae0 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.4" +version: "0.15.6" slug: "gree_controller" description: "Local GREE HVAC controller with Web UI, API, Swagger API docs and Home Assistant integration" -url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/" +url: "https://repo.example.com/gree-controller-ha-addon/" arch: - amd64 - aarch64 -image: "zot.linuxiarz.pl/gree-controller" +image: "repo.example.com/gree-controller" startup: application boot: auto init: false diff --git a/ha-addon/repository/repository.yaml b/ha-addon/repository/repository.yaml index c5b53d9..ac765b2 100644 --- a/ha-addon/repository/repository.yaml +++ b/ha-addon/repository/repository.yaml @@ -1,3 +1,3 @@ name: GREE Controller -url: https://git.linuxiarz.pl/gru/gree-controller-ha-addon/ +url: https://repo.example.com/gree-controller-ha-addon/ maintainer: linuxiarz.pl diff --git a/lang/en.json b/lang/en.json index 5eb405e..a238b3e 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 searches both AES-ECB and AES-GCM devices. Multiple passes improve discovery when several Wi-Fi modules answer the same broadcast.", + "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.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": "Name discovered devices", - "discovery.nameDevicesHelp": "Give each new unit a friendly room name. The technical model and MAC remain available in diagnostics.", + "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.", "dashboard.thermostats": "Thermostats", "common.unavailable": "Unavailable", "mode.off": "Off", @@ -1408,6 +1408,12 @@ "publicChart.field.temperature": "Temperature", "flow.invalidPresetFilename": "Invalid preset filename.", "flow.invalidPresetFile": "{filename}: invalid preset.", - "flow.presetNotFound": "Preset not found." + "flow.presetNotFound": "Preset not found.", + "discovery.addSelected": "Add selected", + "discovery.selectAtLeastOne": "Select at least one device to add.", + "discovery.addedCount": "Added: {count}", + "discovery.alreadyAdded": "Already added", + "discovery.empty": "No local devices found", + "discovery.emptyHint": "Try a longer scan, another protocol, or verify that the unit answers on UDP port 7000." } } diff --git a/lang/pl.json b/lang/pl.json index 5b33f1f..351ac29 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 wyszukuje urządzenia AES-ECB i AES-GCM. Kilka przebiegów zwiększa skuteczność, gdy wiele modułów Wi-Fi odpowiada na ten sam broadcast.", + "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.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": "Nazwij znalezione urządzenia", - "discovery.nameDevicesHelp": "Nadaj każdej nowej jednostce przyjazną nazwę pokoju. Model techniczny i MAC pozostaną dostępne w diagnostyce.", + "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ół.", "dashboard.quickThermostats": "Termostaty", "common.unavailable": "Niedostępne", "mode.off": "Wyłączone", @@ -1408,6 +1408,12 @@ "publicChart.field.temperature": "Temperatura", "flow.invalidPresetFilename": "Nieprawidłowa nazwa pliku presetu.", "flow.invalidPresetFile": "{filename}: nieprawidłowy preset.", - "flow.presetNotFound": "Nie znaleziono presetu." + "flow.presetNotFound": "Nie znaleziono presetu.", + "discovery.addSelected": "Dodaj wybrane", + "discovery.selectAtLeastOne": "Wybierz co najmniej jedno urządzenie do dodania.", + "discovery.addedCount": "Dodano: {count}", + "discovery.alreadyAdded": "Już dodane", + "discovery.empty": "Nie znaleziono urządzeń lokalnych", + "discovery.emptyHint": "Spróbuj dłuższego skanowania, innego protokołu albo sprawdź, czy jednostka odpowiada na UDP 7000." } } diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py index 1d88eeb..d924165 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.4. +"""Developer smoke/integration tests for GREE Controller API 0.15.6. 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.4" +DEFAULT_EXPECTED_VERSION = "0.15.6" 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.4 contract", lambda: test_openapi(client, args.expected_version)) + runner.run("OpenAPI 0.15.6 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.rs b/src/api.rs index 56e7a2b..acfaa36 100644 --- a/src/api.rs +++ b/src/api.rs @@ -3,20 +3,19 @@ use crate::{ error::AppError, home_assistant, influxdb, models::{ - ApiTokenInfo, ApplicationSettings, Automation, ClimateGroup, ConfigurationExport, - ConnectionStatus, ConnectionType, DebugSettings, Device, DeviceCommand, DeviceGroup, + AddDiscoveredDevicesRequest, ApiTokenInfo, ApplicationSettings, Automation, ClimateGroup, + ConfigurationExport, ConnectionStatus, ConnectionType, DebugSettings, Device, DeviceCommand, DeviceGroup, DeviceGroupKind, DevicePatch, DiscoveryRequest, EnergyReading, EnergySourcePreference, Flow, GreeCloudSettings, GreeCloudSettingsUpdate, GreeCloudSettingsView, GreeSettings, GroupControlPatch, HaReading, HistorySettings, HomeAssistantSettings, HomeAssistantSettingsUpdate, HomeAssistantSettingsView, InfluxDbSettings, - InfluxDbSettingsUpdate, InfluxDbSettingsView, ManualDeviceRequest, NetworkReading, + InfluxDbSettingsUpdate, InfluxDbSettingsView, LocalDiscoveryCandidate, ManualDeviceRequest, NetworkReading, NightModeSettings, NotificationSettings, NotificationSettingsUpdate, NotificationSettingsView, Reading, RuntimeSettings, Schedule, SettingsSnapshot, TemporaryQuickThermostat, TemporaryQuickThermostatRequest, Zone, ZoneControlPatch, ZoneReading, }, notifications, - protocol::merge_discovered, state::AppState, }; use axum::{ @@ -87,7 +86,8 @@ pub fn router(state: AppState) -> Router { let protected = Router::new() .route("/api/bootstrap", get(bootstrap)) .route("/api/system/info", get(system_info)) - .route("/api/discovery", post(discover)) + .route("/api/discovery/scan", post(scan_discovery)) + .route("/api/discovery/add", post(add_discovered_devices)) .route("/api/devices", get(list_devices).post(add_device)) .route( "/api/devices/{id}", diff --git a/src/api/devices.rs b/src/api/devices.rs index c69259c..2b13ba5 100644 --- a/src/api/devices.rs +++ b/src/api/devices.rs @@ -1,15 +1,130 @@ -async fn discover( - State(state): State, - Json(request): Json, -) -> Result, AppError> { - let _configuration_guard = state.lock_configuration_operation().await; +fn normalize_local_discovery_mac(value: &str) -> String { + value.replace([':', '-'], "").trim().to_ascii_uppercase() +} + +fn local_discovery_candidate(device: &Device, already_added: bool) -> LocalDiscoveryCandidate { + LocalDiscoveryCandidate { + name: device.name.clone(), + mac: device.mac.clone(), + ip: device.ip.clone(), + port: device.port, + protocol_version: device.protocol_version, + model: device.model.clone(), + firmware: device.firmware.clone(), + already_added, + } +} + +fn device_from_local_discovery(candidate: LocalDiscoveryCandidate) -> Result { + let mac = normalize_local_discovery_mac(&candidate.mac); + if mac.is_empty() { + return Err(AppError::BadRequest("discovered device MAC is required".into())); + } + candidate + .ip + .parse::() + .map_err(|_| AppError::BadRequest(format!("invalid IP address for {mac}")))?; + if !matches!(candidate.protocol_version, 1 | 2) { + return Err(AppError::BadRequest(format!( + "invalid protocol version for {mac}" + ))); + } + + let model = candidate.model.trim().to_string(); + let fallback_model = if model.is_empty() { "GREE" } else { &model }; + let suffix = mac + .chars() + .rev() + .take(4) + .collect::() + .chars() + .rev() + .collect::(); + let name = if candidate.name.trim().is_empty() { + format!("{fallback_model} {suffix}") + } else { + candidate.name.trim().to_string() + }; + let now = Utc::now(); + + Ok(Device { + id: format!("gree-{}", mac.to_ascii_lowercase()), + mac, + name, + connection_type: ConnectionType::Local, + connection_status: ConnectionStatus::Unknown, + cloud_device_id: None, + cloud_parent_mac: None, + cloud_account_id: None, + ip: candidate.ip, + port: if candidate.port == 0 { 7000 } else { candidate.port }, + protocol_version: candidate.protocol_version, + model, + firmware: candidate.firmware.trim().to_string(), + key: None, + cid: Some("app".into()), + enabled: true, + simulated: false, + power: false, + mode: "cool".into(), + target_temperature: 24.0, + fan_speed: 0, + swing_vertical: 0, + swing_horizontal: 0, + quiet: false, + quiet_wire_value: None, + turbo: false, + light: true, + air: false, + xfan: false, + health: false, + sleep: false, + supports_light: None, + supports_quiet: None, + supports_turbo: None, + supports_air: None, + supports_xfan: None, + supports_health: None, + supports_sleep: None, + supports_buzzer_control: None, + supports_energy_meter: None, + total_energy_kwh: None, + compressor_frequency_hz: None, + last_cloud_sync: None, + current_temperature: None, + outdoor_temperature: None, + temperature_sensor_offset: None, + online: true, + response_time_ms: None, + last_seen: Some(now), + last_error: None, + communication_failures: 0, + pending_command: false, + capabilities: crate::models::DeviceCapabilities::default(), + energy_source: EnergySourcePreference::Auto, + ha_energy_entity_id: None, + ha_energy_unit: None, + ha_energy_device_class: None, + ha_energy_state_class: None, + created_at: now, + updated_at: now, + }) +} + +async fn run_local_discovery( + state: &AppState, + request: DiscoveryRequest, +) -> Result<(u8, u8, Vec), AppError> { let settings = state.settings.read().await.clone(); let timeout_ms = request .timeout_ms .unwrap_or(settings.discovery_timeout_ms) .clamp(500, 30_000); let broadcast = request.broadcast.unwrap_or(settings.discovery_broadcast); - let protocol_version = request.protocol_version.unwrap_or(0).min(2); + let protocol_version = request.protocol_version.unwrap_or(0); + if protocol_version > 2 { + return Err(AppError::BadRequest("protocol_version must be 0, 1 or 2".into())); + } let passes = request.passes.unwrap_or(3).clamp(1, 10); let discovered = state .providers @@ -23,52 +138,112 @@ async fn discover( ) .await .map_err(|e| AppError::Device(e.to_string()))?; - let mut saved = Vec::new(); - let mut new_device_ids = Vec::new(); - for item in discovered { - let existing = state.db.get_device_by_mac(&item.mac)?; - let is_new = existing.is_none(); - let mut merged = merge_discovered(existing, item); - let _device_guard = state.lock_device_operation(&merged.id).await; - // A poll/command may have updated the same known device between discovery and - // acquiring its operation lock. Re-merge against the freshest persisted state. - if !is_new { - if let Some(current) = state.db.get_device(&merged.id)? { - merged = merge_discovered(Some(current), merged); - } - } - // Bind right after discovery. GREE modules can have a short bind window; - // bind() also refreshes it with a direct scan before the handshake. - if !merged.simulated && merged.key.as_deref().unwrap_or_default().is_empty() { - match state.providers.local().client().bind(&merged).await { - Ok(bound) => { - merged.key = Some(bound.key); - merged.protocol_version = bound.protocol_version; - merged.communication_failures = 0; - merged.last_error = None; - } - Err(err) => { - merged.last_error = Some(format!("discovered, bind pending: {err}")); - state.log( - "warn", - "device.bind_after_discovery", - &format!("{}: {err}", merged.name), - json!({"device_id": merged.id}), - ); - } - } - } - state.db.save_device(&merged)?; - if is_new { - new_device_ids.push(merged.id.clone()); - } - saved.push(merged); + Ok((protocol_version, passes, discovered)) +} + +/// Scan for local GREE units without persisting or binding them. +async fn scan_discovery( + State(state): State, + Json(request): Json, +) -> Result, AppError> { + let (protocol_version, passes, discovered) = run_local_discovery(&state, request).await?; + let mut candidates = Vec::with_capacity(discovered.len()); + 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)); } - state.log("info", "discovery.complete", &format!("Discovery found {} device(s)", saved.len()), json!({"count": saved.len(), "protocol_version": protocol_version, "passes": passes, "new_devices": new_device_ids.len()})); - state.broadcast("devices.discovered", json!({"devices": saved})); - Ok(Json( - json!({"count": saved.len(), "devices": saved, "new_device_ids": new_device_ids}), - )) + + state.log( + "info", + "discovery.scan_complete", + &format!("Discovery scan found {} device(s)", candidates.len()), + json!({ + "count": candidates.len(), + "protocol_version": protocol_version, + "passes": passes, + "persisted": false, + }), + ); + Ok(Json(json!({ + "count": candidates.len(), + "devices": candidates, + }))) +} + +async fn add_discovered_devices( + State(state): State, + Json(request): Json, +) -> Result, AppError> { + let _configuration_guard = state.lock_configuration_operation().await; + if request.devices.is_empty() { + return Err(AppError::BadRequest("select at least one discovered device".into())); + } + if request.devices.len() > 64 { + return Err(AppError::BadRequest("too many discovered devices selected".into())); + } + + let selected_count = request.devices.len(); + let mut added = Vec::new(); + let mut skipped = Vec::new(); + + for candidate in request.devices { + 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() { + skipped.push(device.mac.clone()); + continue; + } + + match state + .providers + .local() + .client() + .bind_exact(&device, device.protocol_version) + .await + { + Ok(bound) => { + device.key = Some(bound.key); + device.protocol_version = bound.protocol_version; + device.communication_failures = 0; + device.last_error = None; + } + Err(err) => { + device.last_error = Some(format!("added, bind pending: {err}")); + state.log( + "warn", + "device.bind_after_discovery", + &format!("{}: {err}", device.name), + json!({ + "device_id": device.id, + "protocol_version": device.protocol_version, + }), + ); + } + } + + state.db.save_device(&device)?; + added.push(device); + } + + state.log( + "info", + "discovery.devices_added", + &format!("Added {} discovered device(s)", added.len()), + json!({ + "selected": selected_count, + "added": added.len(), + "skipped": skipped.len(), + }), + ); + if !added.is_empty() { + state.broadcast("devices.discovered", json!({"devices": added})); + } + Ok(Json(json!({ + "count": added.len(), + "devices": added, + "skipped_macs": skipped, + }))) } async fn list_devices(State(state): State) -> Result>, AppError> { diff --git a/src/api/openapi.rs b/src/api/openapi.rs index 86278ad..03c2ca5 100644 --- a/src/api/openapi.rs +++ b/src/api/openapi.rs @@ -71,6 +71,9 @@ mod tests { fn every_documented_operation_has_summary_description_and_responses() { let document = document(""); let paths = document["paths"].as_object().expect("OpenAPI paths object"); + assert!(!paths.contains_key("/api/discovery")); + assert!(paths.contains_key("/api/discovery/scan")); + assert!(paths.contains_key("/api/discovery/add")); for (path, item) in paths { let methods = item.as_object().expect("OpenAPI path item"); for (method, operation) in methods { diff --git a/src/models/runtime.rs b/src/models/runtime.rs index 1c2815f..ad9d4b6 100644 --- a/src/models/runtime.rs +++ b/src/models/runtime.rs @@ -117,6 +117,29 @@ pub struct DiscoveryRequest { pub passes: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LocalDiscoveryCandidate { + pub name: String, + pub mac: String, + pub ip: String, + #[serde(default = "default_port")] + pub port: u16, + /// Detected protocol: 1 = AES-ECB, 2 = AES-GCM. + pub protocol_version: u8, + #[serde(default)] + pub model: String, + #[serde(default)] + pub firmware: String, + #[serde(default)] + pub already_added: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddDiscoveredDevicesRequest { + #[serde(default)] + pub devices: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ManualDeviceRequest { pub name: String, diff --git a/src/protocol/gree.rs b/src/protocol/gree.rs index 720ecf6..556a210 100644 --- a/src/protocol/gree.rs +++ b/src/protocol/gree.rs @@ -22,7 +22,6 @@ use tokio::{ sync::broadcast, time::{timeout, Instant}, }; -use uuid::Uuid; #[derive(Debug, Clone)] pub struct BindResult { @@ -51,5 +50,4 @@ include!("gree/polling.rs"); include!("gree/commands.rs"); include!("gree/transport.rs"); include!("gree/network.rs"); -include!("gree/merge.rs"); include!("gree/tests.rs"); diff --git a/src/protocol/gree/binding.rs b/src/protocol/gree/binding.rs index fdc964b..dd30076 100644 --- a/src/protocol/gree/binding.rs +++ b/src/protocol/gree/binding.rs @@ -1,9 +1,16 @@ impl GreeClient { pub async fn bind(&self, device: &Device) -> Result { - let versions: &[u8] = match device.protocol_version { - 2 => &[2, 1], - _ => &[1, 2], - }; + let versions = Self::auto_bind_versions(device.protocol_version); + self.bind_with_versions(device, versions).await + } + + /// Bind using exactly the protocol detected during 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 + } + + async fn bind_with_versions(&self, device: &Device, versions: &[u8]) -> Result { let mut errors = Vec::new(); for &version in versions { match self.bind_attempt(device, version).await { @@ -22,6 +29,18 @@ impl GreeClient { bail!("unable to bind device ({})", errors.join("; ")) } + fn auto_bind_versions(device_protocol: u8) -> &'static [u8] { + if device_protocol == 2 { &[2, 1] } else { &[1, 2] } + } + + fn exact_bind_versions(protocol_version: u8) -> Result<&'static [u8]> { + match protocol_version { + 1 => Ok(&[1]), + 2 => Ok(&[2]), + other => bail!("unsupported GREE protocol version: {other}"), + } + } + /// GREE Wi-Fi modules use the 12-hex device id as a protocol identifier. /// Older V1 modules (notably 502cc6...) can silently ignore bind/status /// packets when tcid/mac casing differs from the lowercase value returned diff --git a/src/protocol/gree/discovery.rs b/src/protocol/gree/discovery.rs index f2fa412..179a64d 100644 --- a/src/protocol/gree/discovery.rs +++ b/src/protocol/gree/discovery.rs @@ -52,11 +52,8 @@ impl GreeClient { let Ok(value) = serde_json::from_slice::(&buffer[..size]) else { continue; }; - match self.parse_discovery(value, source) { + match self.parse_discovery(value, source, protocol_filter) { Ok(Some(mut device)) => { - if protocol_filter != 0 && device.protocol_version != protocol_filter { - continue; - } let key = device.mac.to_ascii_lowercase(); if seen.insert(key) { device.last_seen = Some(Utc::now()); @@ -77,11 +74,24 @@ impl GreeClient { Ok(result) } - fn parse_discovery(&self, mut value: Value, source: SocketAddr) -> Result> { + fn parse_discovery( + &self, + mut value: Value, + source: SocketAddr, + protocol_filter: u8, + ) -> Result> { let mut detected_protocol = 1_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; decrypt_v2(GENERIC_GREE_V2_KEY, pack, tag)? @@ -91,10 +101,16 @@ impl GreeClient { 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(); } } } + if protocol_filter != 0 && detected_protocol != protocol_filter { + return Ok(None); + } let kind = value .get("t") .and_then(Value::as_str) diff --git a/src/protocol/gree/merge.rs b/src/protocol/gree/merge.rs deleted file mode 100644 index 3dd71f3..0000000 --- a/src/protocol/gree/merge.rs +++ /dev/null @@ -1,34 +0,0 @@ -pub fn merge_discovered(existing: Option, discovered: Device) -> Device { - if let Some(mut old) = existing { - old.ip = discovered.ip; - old.port = discovered.port; - if old.name.trim().is_empty() - || old.name == "Klimatyzator GREE" - || old.name == "GREE air conditioner" - { - old.name = discovered.name; - } - if !discovered.model.is_empty() { - old.model = discovered.model; - } - if !discovered.firmware.is_empty() { - old.firmware = discovered.firmware; - } - if old.protocol_version != discovered.protocol_version { - old.protocol_version = discovered.protocol_version; - old.key = None; - } - old.online = true; - old.communication_failures = 0; - old.last_seen = Some(Utc::now()); - old.last_error = None; - old.updated_at = Utc::now(); - old - } else { - let mut new = discovered; - if new.id.is_empty() { - new.id = Uuid::new_v4().to_string(); - } - new - } -} diff --git a/src/protocol/gree/tests.rs b/src/protocol/gree/tests.rs index 9098ba9..b93ca3e 100644 --- a/src/protocol/gree/tests.rs +++ b/src/protocol/gree/tests.rs @@ -118,4 +118,40 @@ mod tests { assert!(device.air); assert_eq!(device.supports_air, Some(true)); } + + #[test] + fn discovery_bind_uses_detected_protocol_exactly() { + 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()); + assert!(GreeClient::exact_bind_versions(3).is_err()); + assert_eq!(GreeClient::auto_bind_versions(1), &[1, 2]); + assert_eq!(GreeClient::auto_bind_versions(2), &[2, 1]); + } + + #[test] + fn discovery_protocol_filter_rejects_other_envelope_before_decode() { + let client = GreeClient::new( + "test-controller".into(), + None, + None, + Arc::new(AtomicBool::new(false)), + ); + let source: SocketAddr = "192.0.2.20:7000".parse().unwrap(); + let v1 = json!({ + "t": "pack", + "pack": { + "t": "dev", + "mac": "AABBCCDDEEFF", + "name": "Test" + } + }); + + assert!(client + .parse_discovery(v1.clone(), source, 1) + .unwrap() + .is_some()); + assert!(client.parse_discovery(v1, source, 2).unwrap().is_none()); + } + } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 332c0a6..1153a9c 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -3,4 +3,4 @@ pub mod gree; pub mod gree_cloud; pub mod gree_cloud_mqtt; -pub use gree::{merge_discovered, GreeClient}; +pub use gree::GreeClient; diff --git a/web/css/styles.css b/web/css/styles.css index a17ad6a..5026dd4 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -1538,6 +1538,34 @@ legend { overflow-wrap: anywhere; } +.local-discovery-row { + gap: 10px; +} + +.local-discovery-head { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 10px; +} + +.local-discovery-head > input[type="checkbox"] { + width: 18px; + height: 18px; + margin: 0; +} + +.local-discovery-head > label { + display: grid; + gap: 2px; + min-width: 0; + cursor: pointer; +} + +.local-discovery-head > .badge { + justify-self: end; +} + * { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--accent) 42%, var(--surface-2)) var(--surface-muted); diff --git a/web/index.html b/web/index.html index 5220b99..a4863c1 100644 --- a/web/index.html +++ b/web/index.html @@ -1263,8 +1263,7 @@

Discover GREE devices

-

Auto searches both AES-ECB and AES-GCM devices. Multiple passes - improve discovery when several Wi-Fi modules answer the same broadcast.

+

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.

`).join(''); + 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 selectorId = `local-discovery-${index}`; + return `
+
+ + + ${alreadyAdded ? `${esc(tr('discovery.alreadyAdded'))}` : ''} +
+ +
`; + }).join('') : `
${esc(tr('discovery.empty'))}${esc(tr('discovery.emptyHint'))}
`; openDialog('discoveryNamesDialog'); }