This commit is contained in:
Mateusz Gruszczyński
2026-09-18 11:21:48 +02:00
parent 3ffb02595e
commit 8d96ad2d38
29 changed files with 758 additions and 256 deletions
Generated
+1 -1
View File
@@ -719,7 +719,7 @@ dependencies = [
[[package]] [[package]]
name = "gree-controller" name = "gree-controller"
version = "0.15.4" version = "0.15.6"
dependencies = [ dependencies = [
"aes", "aes",
"aes-gcm", "aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "gree-controller" name = "gree-controller"
version = "0.15.4" version = "0.15.6"
edition = "2021" edition = "2021"
authors = ["GREE Controller contributors"] authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+27 -27
View File
@@ -1,13 +1,14 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
37e255dd094e4720f96b74e501a179cf81c838ff327cbc1924b8fc4b5ea555fa ./Cargo.toml 03fd19b33f6b87a3cf55957c44c619dc65b6c1ef6ab35a275f7142e063fa130d ./Cargo.lock
0f1ebc69bb39b480c603772736bbc968cdeada29efecca6a056e4ebd0b071cb7 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
c1cac4ca532e73d64e5b090ad410a2161e9790b8d77fc6732adf52c248fb3472 ./README.md 252802f9b072e316a1898b5b39922788c0ed7ab769aba6e1cb33e5ac5595d7fb ./README.md
8dc1f42dd20c98873fcd0a979475560f9ec79e098f01375de47f8fd96ef6e80d ./SOURCE_MANIFEST.sha256 fb12007189dad1c860aeed1eacc561c54afb01204396982b42bfa68706dc583e ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
0da3db314314e19a00c08700f94495075fecd1e54a5b188f6a62cb237307e96f ./docs/API.md c52fb51807e46086e6bb44dd532dde346a742d12d52ce703055f1d4f433fd253 ./docs/API.md
a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
55ffdcc5b0a43d930823cea7c94235990dbbc4a35b60832b9adca1faf606d036 ./docs/openapi.json 524977fb867f2b56b56df827df0b8f646e366c00fa31d62c577a5a118f76406f ./docs/openapi.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
@@ -21,7 +22,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py 6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
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 c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -29,21 +30,21 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json 50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
1d1508e247b67487e6aac52b532adec2842494ec2e60144491c5144967a4f8f3 ./ha-addon/repository/gree-controller/CHANGELOG.md 732265515c1d45eb11b966919cfa29ed9f600b6dd02a05e1abee8afda4151213 ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.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 3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png 0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml 2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
e6b93d3701d83a31506617a0e6f44ff32d8c24a52be038c8540c862b13546b50 ./ha-addon/repository/gree-controller/translations/pl.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 cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run.sh
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
e5c6a6051381e28d74b75e48232de13d02eb87519d29c847b7ab7979ce47d082 ./lang/en.json 2175f4ce8b5d994e1e4f912244b03721a93a41e2970f39826d6ea15fdaa7078e ./lang/en.json
02c650f603bca1bd17d9d59193c176ee6ddccb8304f2e658d7ba7730f8ff5f83 ./lang/pl.json 1055d5835329c03eb6f5b8fab4ee438c76b23d6eebdadf70effa42998c03a279 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -84,7 +85,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
b9bd0f1db6bf879ca3778c4969dd56d3c35b7bdd137df517dbd2cd2e11856080 ./scripts/api_dev_test.py 03d5ee12306c95d5ae5df4a2a3a422fe5044e0961bb2be4b7ee49641a9ee7fc1 ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -97,14 +98,14 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
f4ace9a1104ab1d0016db1d34c3ac2c4010fc1a5534af5fbf02d2042d5815d8c ./src/api.rs b53e1afab342634fedc218ea4517446f19a5ba7985dee599e191b37d9cc8058a ./src/api.rs
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs 9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs 27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs f775aa2f3c5c554e4197d47cf381f3d2133ea4c1430a5510f045188dd41e5e51 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs 2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
@@ -113,7 +114,7 @@ b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/hist
e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs
8495967c3df72110f591c744975984c78cafc8181b2d430e7fd5b9baf57eba95 ./src/api/integrations.rs 8495967c3df72110f591c744975984c78cafc8181b2d430e7fd5b9baf57eba95 ./src/api/integrations.rs
7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs 7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs
c01b8b942e64a46dac19ce08346aa844df7e9ac7147c0ad6d0f2cfd90e071ec0 ./src/api/openapi.rs 9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs
b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs 5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
@@ -169,25 +170,24 @@ fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/c
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs 6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs
1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs 1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs
f1359b68f1d041cd441f80429e77893eeb6f6194613b58fd3e7a212fb511db3e ./src/models/runtime.rs 53caf9ba4667a2ae33d5c3ce69f85005d9f8540f2ee3c16c2cb3795a53e9d21f ./src/models/runtime.rs
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs 41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs 4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs 7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
daca14e9a4dd65394add7c7753c8ae5e7e43e991323a5ca23d8788aae4428925 ./src/protocol/gree.rs 578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs 5f9f46d3ee6d410b8848ad46f2c482159eebfd692e739b0f09941a43c8d97211 ./src/protocol/gree/binding.rs
91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs 91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs
4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs 4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs
668e3bfcc4be84d1a86909bfc18e0d3816e0343cdc0d9b43189f7b0c6e0666d4 ./src/protocol/gree/discovery.rs 034344ab9b86a271854358cfc733d45386c90e8ea66e1f232afd927428422e3a ./src/protocol/gree/discovery.rs
0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs
ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs
1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.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 3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs
b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs
6258ee3888a5f9a4f8bb6cfbbac97f2621de07f9da6bcb2fa596d452f47f2e49 ./src/protocol/mod.rs 5f70211b73215d9ae7af8fc306d15dc0b6cfbf0df9f79e34f1c785957fdf8bd4 ./src/protocol/mod.rs
a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider.rs a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs 830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs 7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
@@ -199,22 +199,22 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs 8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
2822a98a4d7f72f2b89b70878aa0f9bd3f391fde4ea04ba1b94e89a527dcd1fb ./web/css/styles.css 0b783d6cea599e53fed4ed82fa95e2048c493057c6d57b0dfa423e7cfa10a7ad ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
d5115a9dc59b4e753dcff9239a77fe49886951d7a4ae7456be80d9201cc42365 ./web/index.html baff48bb71ba348fe39d52fe8d2bef0614bb0c53c1ff5ee748ce09458b3983b2 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.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 752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.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 8e0f294ceeec1d258edb56271469a8a05381e6443a475f76fbeded6637d17503 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.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 0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
+4 -4
View File
@@ -2,9 +2,9 @@
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API. 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. > [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`. > 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. 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. 2. Open **Devices** and start discovery.
3. Auto protocol mode is recommended; the controller accepts both supported GREE encryption generations. 3. Auto protocol mode accepts both supported GREE encryption generations. Selecting V1 or V2 strictly filters discovery and bind to that generation.
4. Newly discovered devices are bound automatically when possible. Manual bind is also available. 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. 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. Discovery uses UDP broadcast, so routed/VLAN networks must explicitly permit or relay the required traffic.
+26 -26
View File
@@ -1,12 +1,13 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
37e255dd094e4720f96b74e501a179cf81c838ff327cbc1924b8fc4b5ea555fa ./Cargo.toml 03fd19b33f6b87a3cf55957c44c619dc65b6c1ef6ab35a275f7142e063fa130d ./Cargo.lock
0f1ebc69bb39b480c603772736bbc968cdeada29efecca6a056e4ebd0b071cb7 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
c1cac4ca532e73d64e5b090ad410a2161e9790b8d77fc6732adf52c248fb3472 ./README.md 252802f9b072e316a1898b5b39922788c0ed7ab769aba6e1cb33e5ac5595d7fb ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
0da3db314314e19a00c08700f94495075fecd1e54a5b188f6a62cb237307e96f ./docs/API.md c52fb51807e46086e6bb44dd532dde346a742d12d52ce703055f1d4f433fd253 ./docs/API.md
a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
55ffdcc5b0a43d930823cea7c94235990dbbc4a35b60832b9adca1faf606d036 ./docs/openapi.json 524977fb867f2b56b56df827df0b8f646e366c00fa31d62c577a5a118f76406f ./docs/openapi.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
@@ -20,7 +21,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py 6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
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 c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -28,21 +29,21 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json 50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
1d1508e247b67487e6aac52b532adec2842494ec2e60144491c5144967a4f8f3 ./ha-addon/repository/gree-controller/CHANGELOG.md 732265515c1d45eb11b966919cfa29ed9f600b6dd02a05e1abee8afda4151213 ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.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 3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png 0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml 2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
e6b93d3701d83a31506617a0e6f44ff32d8c24a52be038c8540c862b13546b50 ./ha-addon/repository/gree-controller/translations/pl.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 cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run.sh
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
e5c6a6051381e28d74b75e48232de13d02eb87519d29c847b7ab7979ce47d082 ./lang/en.json 2175f4ce8b5d994e1e4f912244b03721a93a41e2970f39826d6ea15fdaa7078e ./lang/en.json
02c650f603bca1bd17d9d59193c176ee6ddccb8304f2e658d7ba7730f8ff5f83 ./lang/pl.json 1055d5835329c03eb6f5b8fab4ee438c76b23d6eebdadf70effa42998c03a279 ./lang/pl.json
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py
b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json
1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json
@@ -83,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
b9bd0f1db6bf879ca3778c4969dd56d3c35b7bdd137df517dbd2cd2e11856080 ./scripts/api_dev_test.py 03d5ee12306c95d5ae5df4a2a3a422fe5044e0961bb2be4b7ee49641a9ee7fc1 ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -96,14 +97,14 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
f4ace9a1104ab1d0016db1d34c3ac2c4010fc1a5534af5fbf02d2042d5815d8c ./src/api.rs b53e1afab342634fedc218ea4517446f19a5ba7985dee599e191b37d9cc8058a ./src/api.rs
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs 9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs 27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs
5245d5a70c4a1919d8b7757db399e1caf692c8b3a8af0343d8e462bf90373137 ./src/api/devices.rs f775aa2f3c5c554e4197d47cf381f3d2133ea4c1430a5510f045188dd41e5e51 ./src/api/devices.rs
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs 2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
@@ -112,7 +113,7 @@ b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/hist
e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs
8495967c3df72110f591c744975984c78cafc8181b2d430e7fd5b9baf57eba95 ./src/api/integrations.rs 8495967c3df72110f591c744975984c78cafc8181b2d430e7fd5b9baf57eba95 ./src/api/integrations.rs
7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs 7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs
c01b8b942e64a46dac19ce08346aa844df7e9ac7147c0ad6d0f2cfd90e071ec0 ./src/api/openapi.rs 9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs
b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs 5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
@@ -168,25 +169,24 @@ fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/c
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs 6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs
1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs 1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs
f1359b68f1d041cd441f80429e77893eeb6f6194613b58fd3e7a212fb511db3e ./src/models/runtime.rs 53caf9ba4667a2ae33d5c3ce69f85005d9f8540f2ee3c16c2cb3795a53e9d21f ./src/models/runtime.rs
41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs 41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs 4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs 7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
daca14e9a4dd65394add7c7753c8ae5e7e43e991323a5ca23d8788aae4428925 ./src/protocol/gree.rs 578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs
7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs 5f9f46d3ee6d410b8848ad46f2c482159eebfd692e739b0f09941a43c8d97211 ./src/protocol/gree/binding.rs
91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs 91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs
4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs 4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs
668e3bfcc4be84d1a86909bfc18e0d3816e0343cdc0d9b43189f7b0c6e0666d4 ./src/protocol/gree/discovery.rs 034344ab9b86a271854358cfc733d45386c90e8ea66e1f232afd927428422e3a ./src/protocol/gree/discovery.rs
0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs
ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol/gree/network.rs
1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.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 3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs
b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs
6258ee3888a5f9a4f8bb6cfbbac97f2621de07f9da6bcb2fa596d452f47f2e49 ./src/protocol/mod.rs 5f70211b73215d9ae7af8fc306d15dc0b6cfbf0df9f79e34f1c785957fdf8bd4 ./src/protocol/mod.rs
a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider.rs a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs 830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs 7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
@@ -198,22 +198,22 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs 8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
2822a98a4d7f72f2b89b70878aa0f9bd3f391fde4ea04ba1b94e89a527dcd1fb ./web/css/styles.css 0b783d6cea599e53fed4ed82fa95e2048c493057c6d57b0dfa423e7cfa10a7ad ./web/css/styles.css
183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
d5115a9dc59b4e753dcff9239a77fe49886951d7a4ae7456be80d9201cc42365 ./web/index.html baff48bb71ba348fe39d52fe8d2bef0614bb0c53c1ff5ee748ce09458b3983b2 ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md
e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js
5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.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 752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js
6b166a9311e30fe948bd3236189436eea769c238f963ba00acec5cdb51ff1dd7 ./web/js-dynamic/entities.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 8e0f294ceeec1d258edb56271469a8a05381e6443a475f76fbeded6637d17503 ./web/js-dynamic/flows.js
3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js
92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js
511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.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 0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js
1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js
c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js
+46 -17
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference # 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) [← Main documentation](../README.md)
@@ -132,7 +132,8 @@ Common statuses:
| Method | Endpoint | Description | | 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. | | GET | `/api/devices` | List devices. |
| POST | `/api/devices` | Add a device manually. | | POST | `/api/devices` | Add a device manually. |
| GET | `/api/devices/{id}` | Read a device. | | GET | `/api/devices/{id}` | Read a device. |
@@ -240,7 +241,7 @@ Response:
{ {
"status": "ok", "status": "ok",
"name": "gree-controller", "name": "gree-controller",
"version": "0.15.4", "version": "0.15.6",
"uptime_seconds": 1234, "uptime_seconds": 1234,
"control_ready": true, "control_ready": true,
"time": "2026-08-30T06:54:00Z" "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": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
"control_plan_revision": 42, "control_plan_revision": 42,
"system": { "system": {
"version": "0.15.4", "version": "0.15.6",
"uptime_seconds": 1234, "uptime_seconds": 1234,
"auth_required": false, "auth_required": false,
"control_ready": true, "control_ready": true,
@@ -374,9 +375,9 @@ A device response contains:
| `communication_failures` | integer | Consecutive/recorded communication failure counter. | | `communication_failures` | integer | Consecutive/recorded communication failure counter. |
| `created_at`, `updated_at` | ISO-8601 | Resource timestamps. | | `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 ```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. The response contains candidates with MAC, IP, detected protocol and an `already_added` flag:
- `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:
```json ```json
{ {
"count": 1, "count": 1,
"devices": [], "devices": [
"new_device_ids": ["gree-aabbccddeeff"] {
"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` ### `GET /api/devices`
Returns `Device[]`. Returns `Device[]`.
@@ -1599,10 +1628,10 @@ BASE='http://127.0.0.1:8787'
AUTH='Authorization: Bearer APP_TOKEN' AUTH='Authorization: Bearer APP_TOKEN'
``` ```
Discover devices: Scan local devices:
```bash ```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}' -d '{"protocol_version":0,"passes":3}'
``` ```
+195 -39
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0", "openapi": "3.1.0",
"info": { "info": {
"title": "GREE Controller API", "title": "GREE Controller API",
"version": "0.15.4", "version": "0.15.6",
"summary": "Local HTTP/WebSocket API for GREE Controller", "summary": "Local HTTP/WebSocket API for GREE Controller",
"description": "Local API used by the GREE Controller Web UI and Home Assistant integration.", "description": "Local API used by the GREE Controller Web UI and Home Assistant integration.",
"license": { "license": {
@@ -198,14 +198,14 @@
} }
} }
}, },
"/api/discovery": { "/api/discovery/scan": {
"post": { "post": {
"tags": [ "tags": [
"Devices" "Devices"
], ],
"summary": "Discover GREE devices", "summary": "Scan for local GREE devices",
"description": "Broadcasts GREE discovery, merges known devices, attempts binding for devices without a key, persists results and reports newly discovered controller IDs.", "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": "discoverDevices", "operationId": "scanDiscoveredDevices",
"requestBody": { "requestBody": {
"required": true, "required": true,
"content": { "content": {
@@ -232,11 +232,77 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Discovery result", "description": "Discovery candidates; no devices are added automatically",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "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": { "version": {
"type": "string", "type": "string",
"example": "0.15.4" "example": "0.15.6"
}, },
"uptime_seconds": { "uptime_seconds": {
"type": "integer", "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": { "TemporaryQuickThermostatRequest": {
"type": "object", "type": "object",
"description": "Persisted temporary thermostat session with delayed/absolute start and several finish rules.", "description": "Persisted temporary thermostat session with delayed/absolute start and several finish rules.",
@@ -9602,6 +9637,127 @@
"source" "source"
], ],
"additionalProperties": false "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": { "responses": {
@@ -1,7 +1,7 @@
{ {
"domain": "gree_controller", "domain": "gree_controller",
"name": "GREE Controller", "name": "GREE Controller",
"version": "0.15.4", "version": "0.15.6",
"config_flow": true, "config_flow": true,
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
@@ -1,5 +1,19 @@
# Changelog # 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 ## 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. - 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.
@@ -1,12 +1,12 @@
name: "GREE Controller" name: "GREE Controller"
version: "0.15.4" version: "0.15.6"
slug: "gree_controller" slug: "gree_controller"
description: "Local GREE HVAC controller with Web UI, API, Swagger API docs and Home Assistant integration" 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: arch:
- amd64 - amd64
- aarch64 - aarch64
image: "zot.linuxiarz.pl/gree-controller" image: "repo.example.com/gree-controller"
startup: application startup: application
boot: auto boot: auto
init: false init: false
+1 -1
View File
@@ -1,3 +1,3 @@
name: GREE Controller 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 maintainer: linuxiarz.pl
+10 -4
View File
@@ -296,14 +296,14 @@
"devices.savedAndChecked": "Saved and connection confirmed", "devices.savedAndChecked": "Saved and connection confirmed",
"devices.savedCheckFailed": "Configuration saved, but the connection test failed", "devices.savedCheckFailed": "Configuration saved, but the connection test failed",
"discovery.title": "Discover GREE devices", "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.protocol": "Discovery protocol",
"discovery.passes": "Scan passes", "discovery.passes": "Scan passes",
"discovery.timeout": "Total scan time (ms)", "discovery.timeout": "Total scan time (ms)",
"zones.quickHint": "Quick target and mode control", "zones.quickHint": "Quick target and mode control",
"actions.later": "Later", "actions.later": "Later",
"discovery.nameDevices": "Name discovered devices", "discovery.nameDevices": "Discovered local devices",
"discovery.nameDevicesHelp": "Give each new unit a friendly room name. The technical model and MAC remain available in diagnostics.", "discovery.nameDevicesHelp": "Select the units you want to add. MAC address, IP address and detected protocol are shown for every result.",
"dashboard.thermostats": "Thermostats", "dashboard.thermostats": "Thermostats",
"common.unavailable": "Unavailable", "common.unavailable": "Unavailable",
"mode.off": "Off", "mode.off": "Off",
@@ -1408,6 +1408,12 @@
"publicChart.field.temperature": "Temperature", "publicChart.field.temperature": "Temperature",
"flow.invalidPresetFilename": "Invalid preset filename.", "flow.invalidPresetFilename": "Invalid preset filename.",
"flow.invalidPresetFile": "{filename}: invalid preset.", "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."
} }
} }
+10 -4
View File
@@ -296,14 +296,14 @@
"devices.savedAndChecked": "Zapisano i potwierdzono połączenie", "devices.savedAndChecked": "Zapisano i potwierdzono połączenie",
"devices.savedCheckFailed": "Zapisano konfigurację, ale test połączenia nie powiódł się", "devices.savedCheckFailed": "Zapisano konfigurację, ale test połączenia nie powiódł się",
"discovery.title": "Wykrywanie urządzeń GREE", "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.protocol": "Protokół wykrywania",
"discovery.passes": "Liczba przebiegów", "discovery.passes": "Liczba przebiegów",
"discovery.timeout": "Łączny czas wyszukiwania (ms)", "discovery.timeout": "Łączny czas wyszukiwania (ms)",
"zones.quickHint": "Szybka zmiana celu i trybu", "zones.quickHint": "Szybka zmiana celu i trybu",
"actions.later": "Później", "actions.later": "Później",
"discovery.nameDevices": "Nazwij znalezione urządzenia", "discovery.nameDevices": "Wyszukane urządzenia lokalne",
"discovery.nameDevicesHelp": "Nadaj każdej nowej jednostce przyjazną nazwę pokoju. Model techniczny i MAC pozostaną dostępne w diagnostyce.", "discovery.nameDevicesHelp": "Wybierz jednostki, które chcesz dodać. Dla każdego wyniku widoczny jest adres MAC, adres IP i wykryty protokół.",
"dashboard.quickThermostats": "Termostaty", "dashboard.quickThermostats": "Termostaty",
"common.unavailable": "Niedostępne", "common.unavailable": "Niedostępne",
"mode.off": "Wyłączone", "mode.off": "Wyłączone",
@@ -1408,6 +1408,12 @@
"publicChart.field.temperature": "Temperatura", "publicChart.field.temperature": "Temperatura",
"flow.invalidPresetFilename": "Nieprawidłowa nazwa pliku presetu.", "flow.invalidPresetFilename": "Nieprawidłowa nazwa pliku presetu.",
"flow.invalidPresetFile": "{filename}: nieprawidłowy preset.", "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."
} }
} }
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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. Default mode is read-only and safe to run against a real controller.
Use --settings-write to additionally round-trip all split settings resources and Use --settings-write to additionally round-trip all split settings resources and
@@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional
DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787") DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787")
DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "") DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "")
DEFAULT_EXPECTED_VERSION = "0.15.4" DEFAULT_EXPECTED_VERSION = "0.15.6"
SETTINGS_PATHS: dict[str, set[str]] = { SETTINGS_PATHS: dict[str, set[str]] = {
"/api/settings/application": {"simulator_enabled"}, "/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") print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n")
runner.run("public health", lambda: test_health(client, args.expected_version)) runner.run("public health", lambda: test_health(client, args.expected_version))
runner.run("OpenAPI 0.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("bootstrap snapshot contract", lambda: test_bootstrap_contract(client))
runner.run("protected API requires auth", lambda: test_protected_auth(client)) runner.run("protected API requires auth", lambda: test_protected_auth(client))
+5 -5
View File
@@ -3,20 +3,19 @@ use crate::{
error::AppError, error::AppError,
home_assistant, influxdb, home_assistant, influxdb,
models::{ models::{
ApiTokenInfo, ApplicationSettings, Automation, ClimateGroup, ConfigurationExport, AddDiscoveredDevicesRequest, ApiTokenInfo, ApplicationSettings, Automation, ClimateGroup,
ConnectionStatus, ConnectionType, DebugSettings, Device, DeviceCommand, DeviceGroup, ConfigurationExport, ConnectionStatus, ConnectionType, DebugSettings, Device, DeviceCommand, DeviceGroup,
DeviceGroupKind, DevicePatch, DiscoveryRequest, EnergyReading, EnergySourcePreference, DeviceGroupKind, DevicePatch, DiscoveryRequest, EnergyReading, EnergySourcePreference,
Flow, GreeCloudSettings, GreeCloudSettingsUpdate, GreeCloudSettingsView, GreeSettings, Flow, GreeCloudSettings, GreeCloudSettingsUpdate, GreeCloudSettingsView, GreeSettings,
GroupControlPatch, HaReading, HistorySettings, HomeAssistantSettings, GroupControlPatch, HaReading, HistorySettings, HomeAssistantSettings,
HomeAssistantSettingsUpdate, HomeAssistantSettingsView, InfluxDbSettings, HomeAssistantSettingsUpdate, HomeAssistantSettingsView, InfluxDbSettings,
InfluxDbSettingsUpdate, InfluxDbSettingsView, ManualDeviceRequest, NetworkReading, InfluxDbSettingsUpdate, InfluxDbSettingsView, LocalDiscoveryCandidate, ManualDeviceRequest, NetworkReading,
NightModeSettings, NotificationSettings, NotificationSettingsUpdate, NightModeSettings, NotificationSettings, NotificationSettingsUpdate,
NotificationSettingsView, Reading, RuntimeSettings, Schedule, SettingsSnapshot, NotificationSettingsView, Reading, RuntimeSettings, Schedule, SettingsSnapshot,
TemporaryQuickThermostat, TemporaryQuickThermostatRequest, Zone, ZoneControlPatch, TemporaryQuickThermostat, TemporaryQuickThermostatRequest, Zone, ZoneControlPatch,
ZoneReading, ZoneReading,
}, },
notifications, notifications,
protocol::merge_discovered,
state::AppState, state::AppState,
}; };
use axum::{ use axum::{
@@ -87,7 +86,8 @@ pub fn router(state: AppState) -> Router {
let protected = Router::new() let protected = Router::new()
.route("/api/bootstrap", get(bootstrap)) .route("/api/bootstrap", get(bootstrap))
.route("/api/system/info", get(system_info)) .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", get(list_devices).post(add_device))
.route( .route(
"/api/devices/{id}", "/api/devices/{id}",
+226 -51
View File
@@ -1,15 +1,130 @@
async fn discover( fn normalize_local_discovery_mac(value: &str) -> String {
State(state): State<AppState>, value.replace([':', '-'], "").trim().to_ascii_uppercase()
Json(request): Json<DiscoveryRequest>, }
) -> Result<Json<Value>, AppError> {
let _configuration_guard = state.lock_configuration_operation().await; 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<Device, AppError> {
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::<IpAddr>()
.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::<String>()
.chars()
.rev()
.collect::<String>();
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<Device>), AppError> {
let settings = state.settings.read().await.clone(); let settings = state.settings.read().await.clone();
let timeout_ms = request let timeout_ms = request
.timeout_ms .timeout_ms
.unwrap_or(settings.discovery_timeout_ms) .unwrap_or(settings.discovery_timeout_ms)
.clamp(500, 30_000); .clamp(500, 30_000);
let broadcast = request.broadcast.unwrap_or(settings.discovery_broadcast); 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 passes = request.passes.unwrap_or(3).clamp(1, 10);
let discovered = state let discovered = state
.providers .providers
@@ -23,52 +138,112 @@ async fn discover(
) )
.await .await
.map_err(|e| AppError::Device(e.to_string()))?; .map_err(|e| AppError::Device(e.to_string()))?;
let mut saved = Vec::new(); Ok((protocol_version, passes, discovered))
let mut new_device_ids = Vec::new(); }
for item in discovered {
let existing = state.db.get_device_by_mac(&item.mac)?; /// Scan for local GREE units without persisting or binding them.
let is_new = existing.is_none(); async fn scan_discovery(
let mut merged = merge_discovered(existing, item); State(state): State<AppState>,
let _device_guard = state.lock_device_operation(&merged.id).await; Json(request): Json<DiscoveryRequest>,
// A poll/command may have updated the same known device between discovery and ) -> Result<Json<Value>, AppError> {
// acquiring its operation lock. Re-merge against the freshest persisted state. let (protocol_version, passes, discovered) = run_local_discovery(&state, request).await?;
if !is_new { let mut candidates = Vec::with_capacity(discovered.len());
if let Some(current) = state.db.get_device(&merged.id)? { for device in discovered {
merged = merge_discovered(Some(current), merged); 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));
// 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);
} }
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})); state.log(
Ok(Json( "info",
json!({"count": saved.len(), "devices": saved, "new_device_ids": new_device_ids}), "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<AppState>,
Json(request): Json<AddDiscoveredDevicesRequest>,
) -> Result<Json<Value>, 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<AppState>) -> Result<Json<Vec<Device>>, AppError> { async fn list_devices(State(state): State<AppState>) -> Result<Json<Vec<Device>>, AppError> {
+3
View File
@@ -71,6 +71,9 @@ mod tests {
fn every_documented_operation_has_summary_description_and_responses() { fn every_documented_operation_has_summary_description_and_responses() {
let document = document(""); let document = document("");
let paths = document["paths"].as_object().expect("OpenAPI paths object"); 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 { for (path, item) in paths {
let methods = item.as_object().expect("OpenAPI path item"); let methods = item.as_object().expect("OpenAPI path item");
for (method, operation) in methods { for (method, operation) in methods {
+23
View File
@@ -117,6 +117,29 @@ pub struct DiscoveryRequest {
pub passes: Option<u8>, pub passes: Option<u8>,
} }
#[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<LocalDiscoveryCandidate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManualDeviceRequest { pub struct ManualDeviceRequest {
pub name: String, pub name: String,
-2
View File
@@ -22,7 +22,6 @@ use tokio::{
sync::broadcast, sync::broadcast,
time::{timeout, Instant}, time::{timeout, Instant},
}; };
use uuid::Uuid;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BindResult { pub struct BindResult {
@@ -51,5 +50,4 @@ include!("gree/polling.rs");
include!("gree/commands.rs"); include!("gree/commands.rs");
include!("gree/transport.rs"); include!("gree/transport.rs");
include!("gree/network.rs"); include!("gree/network.rs");
include!("gree/merge.rs");
include!("gree/tests.rs"); include!("gree/tests.rs");
+23 -4
View File
@@ -1,9 +1,16 @@
impl GreeClient { impl GreeClient {
pub async fn bind(&self, device: &Device) -> Result<BindResult> { pub async fn bind(&self, device: &Device) -> Result<BindResult> {
let versions: &[u8] = match device.protocol_version { let versions = Self::auto_bind_versions(device.protocol_version);
2 => &[2, 1], self.bind_with_versions(device, versions).await
_ => &[1, 2], }
};
/// Bind using exactly the protocol detected during discovery.
pub async fn bind_exact(&self, device: &Device, protocol_version: u8) -> Result<BindResult> {
let versions = Self::exact_bind_versions(protocol_version)?;
self.bind_with_versions(device, versions).await
}
async fn bind_with_versions(&self, device: &Device, versions: &[u8]) -> Result<BindResult> {
let mut errors = Vec::new(); let mut errors = Vec::new();
for &version in versions { for &version in versions {
match self.bind_attempt(device, version).await { match self.bind_attempt(device, version).await {
@@ -22,6 +29,18 @@ impl GreeClient {
bail!("unable to bind device ({})", errors.join("; ")) 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. /// GREE Wi-Fi modules use the 12-hex device id as a protocol identifier.
/// Older V1 modules (notably 502cc6...) can silently ignore bind/status /// Older V1 modules (notably 502cc6...) can silently ignore bind/status
/// packets when tcid/mac casing differs from the lowercase value returned /// packets when tcid/mac casing differs from the lowercase value returned
+21 -5
View File
@@ -52,11 +52,8 @@ impl GreeClient {
let Ok(value) = serde_json::from_slice::<Value>(&buffer[..size]) else { let Ok(value) = serde_json::from_slice::<Value>(&buffer[..size]) else {
continue; continue;
}; };
match self.parse_discovery(value, source) { match self.parse_discovery(value, source, protocol_filter) {
Ok(Some(mut device)) => { Ok(Some(mut device)) => {
if protocol_filter != 0 && device.protocol_version != protocol_filter {
continue;
}
let key = device.mac.to_ascii_lowercase(); let key = device.mac.to_ascii_lowercase();
if seen.insert(key) { if seen.insert(key) {
device.last_seen = Some(Utc::now()); device.last_seen = Some(Utc::now());
@@ -77,11 +74,24 @@ impl GreeClient {
Ok(result) Ok(result)
} }
fn parse_discovery(&self, mut value: Value, source: SocketAddr) -> Result<Option<Device>> { fn parse_discovery(
&self,
mut value: Value,
source: SocketAddr,
protocol_filter: u8,
) -> Result<Option<Device>> {
let mut detected_protocol = 1_u8; let mut detected_protocol = 1_u8;
if value.get("t").and_then(Value::as_str) == Some("pack") { if value.get("t").and_then(Value::as_str) == Some("pack") {
if let Some(pack_value) = value.get("pack") { if let Some(pack_value) = value.get("pack") {
if let Some(pack) = pack_value.as_str() { 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) { let clear = if let Some(tag) = value.get("tag").and_then(Value::as_str) {
detected_protocol = 2; detected_protocol = 2;
decrypt_v2(GENERIC_GREE_V2_KEY, pack, tag)? decrypt_v2(GENERIC_GREE_V2_KEY, pack, tag)?
@@ -91,10 +101,16 @@ impl GreeClient {
value = serde_json::from_slice::<Value>(&clear) value = serde_json::from_slice::<Value>(&clear)
.context("invalid decrypted discovery JSON")?; .context("invalid decrypted discovery JSON")?;
} else if pack_value.is_object() { } else if pack_value.is_object() {
if protocol_filter == 2 {
return Ok(None);
}
value = pack_value.clone(); value = pack_value.clone();
} }
} }
} }
if protocol_filter != 0 && detected_protocol != protocol_filter {
return Ok(None);
}
let kind = value let kind = value
.get("t") .get("t")
.and_then(Value::as_str) .and_then(Value::as_str)
-34
View File
@@ -1,34 +0,0 @@
pub fn merge_discovered(existing: Option<Device>, 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
}
}
+36
View File
@@ -118,4 +118,40 @@ mod tests {
assert!(device.air); assert!(device.air);
assert_eq!(device.supports_air, Some(true)); 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());
}
} }
+1 -1
View File
@@ -3,4 +3,4 @@ pub mod gree;
pub mod gree_cloud; pub mod gree_cloud;
pub mod gree_cloud_mqtt; pub mod gree_cloud_mqtt;
pub use gree::{merge_discovered, GreeClient}; pub use gree::GreeClient;
+28
View File
@@ -1538,6 +1538,34 @@ legend {
overflow-wrap: anywhere; 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-width: thin;
scrollbar-color: color-mix(in srgb, var(--accent) 42%, var(--surface-2)) var(--surface-muted); scrollbar-color: color-mix(in srgb, var(--accent) 42%, var(--surface-2)) var(--surface-muted);
+5 -7
View File
@@ -1263,8 +1263,7 @@
<h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close <h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close
data-ui-icon="close"></button> data-ui-icon="close"></button>
</div> </div>
<p class="field-note" data-i18n="discovery.help">Auto searches both AES-ECB and AES-GCM devices. Multiple passes <p class="field-note" data-i18n="discovery.help">Auto accepts both AES-ECB and AES-GCM devices. Selecting V1 or V2 accepts and binds only that protocol. Multiple passes improve discovery when several Wi-Fi modules answer the same broadcast.</p>
improve discovery when several Wi-Fi modules answer the same broadcast.</p>
<label><span data-i18n="discovery.protocol">Discovery protocol</span><select name="protocol_version"> <label><span data-i18n="discovery.protocol">Discovery protocol</span><select name="protocol_version">
<option value="0" data-i18n="devices.protocolAuto">Auto (V1 + V2)</option> <option value="0" data-i18n="devices.protocolAuto">Auto (V1 + V2)</option>
<option value="1">V1 AES-ECB</option> <option value="1">V1 AES-ECB</option>
@@ -1282,15 +1281,14 @@
<dialog id="discoveryNamesDialog"> <dialog id="discoveryNamesDialog">
<form method="dialog" id="discoveryNamesForm" class="dialog-form"> <form method="dialog" id="discoveryNamesForm" class="dialog-form">
<div class="dialog-head"> <div class="dialog-head">
<h2 data-i18n="discovery.nameDevices">Name discovered devices</h2><button type="button" data-close <h2 data-i18n="discovery.nameDevices">Discovered local devices</h2><button type="button" data-close
data-ui-icon="close"></button> data-ui-icon="close"></button>
</div> </div>
<p class="field-note" data-i18n="discovery.nameDevicesHelp">Give each new unit a friendly room name. The technical <p class="field-note" data-i18n="discovery.nameDevicesHelp">Select the units you want to add. MAC address, IP address and detected protocol are shown for every result.</p>
model and MAC remain available in diagnostics.</p>
<div id="discoveryNamesList" class="discovery-name-list"></div> <div id="discoveryNamesList" class="discovery-name-list"></div>
<div class="form-actions"><button type="button" class="secondary" data-close <div class="form-actions"><button type="button" class="secondary" data-close
data-i18n="actions.later">Later</button><button class="primary" type="submit" data-i18n="actions.cancel">Cancel</button><button class="primary" type="submit"
data-i18n="actions.save">Save</button></div> data-i18n="discovery.addSelected">Add selected</button></div>
</form> </form>
</dialog> </dialog>
+1
View File
@@ -37,6 +37,7 @@ const app = {
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(), simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(),
pingMonitor: { targetId: '', all: false, running: false, timer: null, inFlight: false, samples: {} }, pingMonitor: { targetId: '', all: false, running: false, timer: null, inFlight: false, samples: {} },
flowDraft: null, flowSelectedNodeId: null, flowSelectedNodeIds: [], flowConnectFrom: null, flowDirty: false, flowZoom: 1, flowDraft: null, flowSelectedNodeId: null, flowSelectedNodeIds: [], flowConnectFrom: null, flowDirty: false, flowZoom: 1,
localDiscoveryCandidates: [],
}; };
try { app.savedCharts = JSON.parse(localStorage.getItem('gree_controller_saved_charts') || '[]'); } catch (_) { app.savedCharts = []; } try { app.savedCharts = JSON.parse(localStorage.getItem('gree_controller_saved_charts') || '[]'); } catch (_) { app.savedCharts = []; }
+24 -7
View File
@@ -366,22 +366,39 @@ $('#tokenForm').addEventListener('submit', async event => {
$('#discoverForm').addEventListener('submit', async event => { $('#discoverForm').addEventListener('submit', async event => {
event.preventDefault(); const form = event.currentTarget, raw = Object.fromEntries(new FormData(form)); event.preventDefault(); const form = event.currentTarget, raw = Object.fromEntries(new FormData(form));
await runFormTask(form, async () => { await runFormTask(form, async () => {
const result = await api('/api/discovery', { method: 'POST', body: { protocol_version: Number(raw.protocol_version), passes: Number(raw.passes), timeout_ms: Number(raw.timeout_ms) } }); const protocolVersion = Number(raw.protocol_version);
form.closest('dialog').close(); await loadBootstrap(); toast(tr('toast.found', { count: result.count })); showDiscoveryNames(result.new_device_ids || []); const result = await api('/api/discovery/scan', { method: 'POST', body: { protocol_version: protocolVersion, passes: Number(raw.passes), timeout_ms: Number(raw.timeout_ms) } });
form.closest('dialog').close();
toast(tr('toast.found', { count: result.count }));
showDiscoveryNames(result.devices || []);
}, { busyKey: 'actions.discovering' }); }, { busyKey: 'actions.discovering' });
}); });
$('#discoveryNamesForm').addEventListener('submit', async event => { $('#discoveryNamesForm').addEventListener('submit', async event => {
event.preventDefault(); event.preventDefault();
const form = event.currentTarget; const form = event.currentTarget;
const inputs = $$('input[data-device-id]', form); const selected = $$('[data-discovery-select]:checked', form);
if (!selected.length) {
toast(tr('discovery.selectAtLeastOne'), true);
return;
}
await runFormTask(form, async () => { await runFormTask(form, async () => {
const updated = await Promise.all(inputs.map(input => api(`/api/devices/${encodeURIComponent(input.dataset.deviceId)}`, { method: 'PATCH', body: { name: input.value.trim() } }))); const devices = selected.map(input => {
updated.forEach(updateDevice); const index = Number(input.dataset.discoveryIndex);
const candidate = app.localDiscoveryCandidates[index];
const nameInput = $(`[data-discovery-name="${index}"]`, form);
return { ...candidate, name: String(nameInput?.value || candidate?.name || '').trim() };
}).filter(Boolean);
const result = await api('/api/discovery/add', {
method: 'POST',
body: { devices },
});
await loadBootstrap();
form.closest('dialog').close(); form.closest('dialog').close();
app.localDiscoveryCandidates = [];
renderAll(); renderAll();
toast(tr('common.saved')); toast(tr('discovery.addedCount', { count: result.count }));
}); }, { validate: false });
}); });
$('#renameDeviceForm').addEventListener('submit', async event => { $('#renameDeviceForm').addEventListener('submit', async event => {
+20 -9
View File
@@ -469,16 +469,27 @@ function beginInlineTemperatureEdit(target) {
input.addEventListener('blur', () => finish(true), { once: true }); input.addEventListener('blur', () => finish(true), { once: true });
} }
function showDiscoveryNames(ids) { function showDiscoveryNames(devices) {
const wanted = new Set(Array.isArray(ids) ? ids : []); const items = Array.isArray(devices) ? devices : [];
const devices = app.devices.filter(device => wanted.has(device.id)); app.localDiscoveryCandidates = items.map(item => ({ ...item }));
if (!devices.length) return;
const list = $('#discoveryNamesList'); const list = $('#discoveryNamesList');
list.innerHTML = devices.map(device => ` if (!list) return;
<label class="discovery-name-row"> list.innerHTML = items.length ? items.map((device, index) => {
<span><strong>${esc(device.model || 'GREE')}</strong><small>${esc(device.ip)} · ${esc(device.mac)} · ${device.protocol_version === 2 ? 'V2 GCM' : 'V1 ECB'}</small></span> const alreadyAdded = device.already_added === true;
<input data-device-id="${esc(device.id)}" maxlength="80" required value="${esc(device.name)}"> const protocol = Number(device.protocol_version) === 2 ? 'V2 AES-GCM' : 'V1 AES-ECB';
</label>`).join(''); const selectorId = `local-discovery-${index}`;
return `<div class="discovery-name-row local-discovery-row">
<div class="local-discovery-head">
<input type="checkbox" id="${esc(selectorId)}" data-discovery-select data-discovery-index="${index}" ${alreadyAdded ? 'disabled' : 'checked'}>
<label for="${esc(selectorId)}">
<strong>${esc(device.model || device.name || 'GREE')}</strong>
<small>IP ${esc(device.ip || '—')} · MAC ${esc(device.mac || '—')} · ${esc(protocol)}</small>
</label>
${alreadyAdded ? `<span class="badge">${esc(tr('discovery.alreadyAdded'))}</span>` : ''}
</div>
<input data-discovery-name="${index}" maxlength="80" value="${esc(device.name || device.model || 'GREE')}" ${alreadyAdded ? 'disabled' : ''} aria-label="${esc(tr('common.name'))}">
</div>`;
}).join('') : `<div class="empty"><strong>${esc(tr('discovery.empty'))}</strong><span>${esc(tr('discovery.emptyHint'))}</span></div>`;
openDialog('discoveryNamesDialog'); openDialog('discoveryNamesDialog');
} }