This commit is contained in:
Mateusz Gruszczyński
2026-09-18 10:27:39 +02:00
parent 7d9e6c8189
commit 3ffb02595e
28 changed files with 610 additions and 480 deletions
Generated
+395 -295
View File
File diff suppressed because it is too large Load Diff
+29 -29
View File
@@ -1,44 +1,44 @@
[package]
name = "gree-controller"
version = "0.15.2"
version = "0.15.4"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
license = "MIT"
[dependencies]
aes = "0.8"
aes-gcm = "0.10"
anyhow = "1"
axum = { version = "0.7", features = ["macros", "ws"] }
base64 = "0.22"
chrono = { version = "0.4", features = ["serde", "clock"] }
clap = { version = "4", features = ["derive", "env"] }
dotenvy = "0.15"
futures-util = "0.3"
libc = "0.2"
rand = "0.8"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
rusqlite = { version = "0.32", features = ["bundled"] }
webpki-roots = "1"
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12", "logging"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
thiserror = "2"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.6", features = ["compression-gzip", "cors", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
utoipa-swagger-ui = { version = "=8.0.3", default-features = false, features = ["axum", "debug-embed", "vendored"] }
url = "2"
uuid = { version = "1", features = ["v4", "serde"] }
aes = "=0.9.3"
aes-gcm = "=0.11.1"
anyhow = "=1.0.104"
axum = { version = "=0.8.9", features = ["macros", "ws"] }
base64 = "=0.23.1"
chrono = { version = "=0.4.45", features = ["serde", "clock"] }
clap = { version = "=4.6.7", features = ["derive", "env"] }
dotenvy = "=0.15.7"
futures-util = "=0.3.34"
libc = "=0.2.189"
rand = "=0.10.2"
reqwest = { version = "=0.13.5", default-features = false, features = ["json", "query", "form", "rustls-no-provider"] }
rusqlite = { version = "=0.40.2", features = ["bundled"] }
webpki-roots = "=1.0.9"
tokio-rustls = { version = "=0.26.5", default-features = false, features = ["ring", "tls12", "logging"] }
serde = { version = "=1.0.229", features = ["derive"] }
serde_json = "=1.0.151"
sha2 = "=0.11.0"
thiserror = "=2.0.20"
tokio = { version = "=1.53.1", features = ["full"] }
tower-http = { version = "=0.7.1", features = ["compression-gzip", "cors", "trace"] }
tracing = "=0.1.44"
tracing-subscriber = { version = "=0.3.23", features = ["env-filter", "fmt"] }
utoipa-swagger-ui = { version = "=9.0.2", default-features = false, features = ["axum", "debug-embed", "vendored"] }
url = "=2.5.8"
uuid = { version = "=1.26.1", features = ["v4", "serde"] }
[build-dependencies]
serde_json = "1"
serde_json = "=1.0.151"
[dev-dependencies]
tempfile = "3"
tempfile = "=3.27.0"
[profile.release]
strip = true
+26 -27
View File
@@ -1,19 +1,18 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
82bb41f94fdf5c66ebd9e6caa92e0f1794431067298e0a27c95c8e5bfd68de89 ./Cargo.lock
39bdb16196253559e3487aa912ff770c09c7b08cdfbb21e9db1d8af1304ea7e0 ./Cargo.toml
37e255dd094e4720f96b74e501a179cf81c838ff327cbc1924b8fc4b5ea555fa ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
84a0803bc75dc3199e23ef56c11c6950e8bd888c686ecda7cc369829e8e3073f ./README.md
f21732756af313565bfe0788230e45cc11c384cee1187f006d426c5463183c0c ./SOURCE_MANIFEST.sha256
c1cac4ca532e73d64e5b090ad410a2161e9790b8d77fc6732adf52c248fb3472 ./README.md
8dc1f42dd20c98873fcd0a979475560f9ec79e098f01375de47f8fd96ef6e80d ./SOURCE_MANIFEST.sha256
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
604a795dec077dd7e4f3462a564fdda68e8084aef07da0fcf3dfb6053a46cc58 ./docs/API.md
0da3db314314e19a00c08700f94495075fecd1e54a5b188f6a62cb237307e96f ./docs/API.md
a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
8babaa6830fa70b2d3d8208ef51808e79c9d68c4e007b9ba1537fe582ba03b3d ./docs/openapi.json
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile
adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md
dd9291b12afade6fc20d66e80a5513251270e18e4c265260b2bbbe393a5a94c4 ./ha-addon/build.sh
4061846cec4d829e4101b4021df7f07dc2e0293ccfd11e91589f49733ec7c5b4 ./ha-addon/home-assistant/README.md
55ffdcc5b0a43d930823cea7c94235990dbbc4a35b60832b9adca1faf606d036 ./docs/openapi.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
a4596a70e50acfe698a8ae1dcb9bebd04adba95b69781218fca7add82c17703e ./ha-addon/build.sh
7dd2534a6f8405e640e28b7cb0e94270cc5f01fad8a972193ec254befe8a96b9 ./ha-addon/home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py
3f6ef15ef58456376ac53fde7cace1ef359d6a6f7a64c5b575ad77ea6e55ccc4 ./ha-addon/home-assistant/custom_components/gree_controller/api.py
da2a0a9390ab75cef150cd281bc329fa409d0d28f51382b36bd8de3a73325c16 ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
@@ -22,7 +21,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
d467809d894b8cb9e8e04a2cc279e0cec3227c1d1c689098bdc3c0380aa9043e ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
d1ec103c9a167382e33877183b6df221a5cced5db58cf5d6eefd2eb24ab16077 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -30,10 +29,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
e3a227ffe23368b152d0ba4a4e81a95b70d35e9870b2a8dc7b2048d6901a7f05 ./ha-addon/repository/gree-controller/CHANGELOG.md
1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
21b82216d0d84cc66ecbed69457caf61174fa9304528fe051c98e8ef7c634025 ./ha-addon/repository/gree-controller/config.yaml
1d1508e247b67487e6aac52b532adec2842494ec2e60144491c5144967a4f8f3 ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
d060ee39cdea4de68bd9ededaebb7b81d974821e418b69e00c39cbdebd61f801 ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -85,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
81f2d3f512f21d167c6d18bd2bda9d9e1557c0b234aa7826148ea557d726ddc2 ./scripts/api_dev_test.py
b9bd0f1db6bf879ca3778c4969dd56d3c35b7bdd137df517dbd2cd2e11856080 ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -98,9 +97,9 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
bd7e0455c6a1501f761f0d4c00ff74833eecbd75078af9168235f90a2d9485b3 ./src/api.rs
f4ace9a1104ab1d0016db1d34c3ac2c4010fc1a5534af5fbf02d2042d5815d8c ./src/api.rs
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
b24092f34a46626d64628b510eba82193f0b203fd097f6a621b91af5d91eb571 ./src/api/auth.rs
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
@@ -110,16 +109,16 @@ ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/devi
2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
3cc64b9c992805f3fbf01c0785cf29ce3121750bf30bd6420ed6e5cf2f8df422 ./src/api/history.rs
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs
8495967c3df72110f591c744975984c78cafc8181b2d430e7fd5b9baf57eba95 ./src/api/integrations.rs
7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs
45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs
c01b8b942e64a46dac19ce08346aa844df7e9ac7147c0ad6d0f2cfd90e071ec0 ./src/api/openapi.rs
b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
bb57f4b28df75ba350c9706719650fff3a689afee0cb6979c1fcae79396ac0cf ./src/api/system.rs
94d3845def693aafe15fa6e8b390881c46c3c236b62f247ddfdc055d9226ed22 ./src/api/websocket.rs
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
ddd1bfc8625b66f6e5f3d24b3122ac86c25f89b0af527e3dfce9102551a8f142 ./src/api/zones.rs
0b7d2ace2e8cb1dd0cb59aa6ca3e80cec14078e93530fd7b58081798585cd833 ./src/config.rs
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
@@ -156,12 +155,12 @@ af4edaf12faf294f44a8992d120a99b09edd1cbdd66981390e027d13fd6b6790 ./src/engine/t
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
fb169bcc20f95abe9892367df6dd3a683584bdbc721792d6ffb9311b81ca30d9 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
c55d32f288fe95869fc288d47817b8e289fbe284be4219e81b1e7493ff5e3a57 ./src/home_assistant.rs
a9768150162a010579d40060417e276703e8756c139fcf57dddab9fffc98d840 ./src/home_assistant.rs
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
2d36405912646b30b1a1a37a7a4e18c2104ab22e865b593316a609dda884aec2 ./src/influxdb/query.rs
67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs
1982ce54737a8780d1404b105d53db59cd5bee3d1c426f9a881d26cef7059bf5 ./src/main.rs
62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs
23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs
920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs
fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs
@@ -175,7 +174,7 @@ f1359b68f1d041cd441f80429e77893eeb6f6194613b58fd3e7a212fb511db3e ./src/models/r
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
daca14e9a4dd65394add7c7753c8ae5e7e43e991323a5ca23d8788aae4428925 ./src/protocol/gree.rs
7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs
91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs
@@ -186,10 +185,10 @@ ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol
1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs
e0555fac67d1dd0129ecfea6d1fd2fbeb1a73ac89ad95d1b74a7094e50e29ae4 ./src/protocol/gree/tests.rs
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
297983059c915057869fbb4f6bcfe752f3d5bd1e93c3a2651a1bf883c739ed29 ./src/protocol/gree_cloud.rs
b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs
b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs
6258ee3888a5f9a4f8bb6cfbbac97f2621de07f9da6bcb2fa596d452f47f2e49 ./src/protocol/mod.rs
2a08099b456847f24a828d711c4a0aba3fc372936d9a53f0be931f6706226cc5 ./src/provider.rs
a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
580917b91441fe9b3276cf0cfc31536ba7954b6a86f4080347b93f9367a5b1ac ./src/queries/entities.rs
+3 -2
View File
@@ -2,11 +2,12 @@
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.2**
**Version: 0.15.4**
Version 0.15.2 adds granular single-function unit actions to Visual Flow and exposes fan/Quiet/Turbo/Panel Light/Air/X-Fan/Health/Sleep controls in legacy automations, so automations can change one unit feature without touching unrelated settings.
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.
> [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`.
## What it does
+25 -26
View File
@@ -1,18 +1,17 @@
de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
82bb41f94fdf5c66ebd9e6caa92e0f1794431067298e0a27c95c8e5bfd68de89 ./Cargo.lock
39bdb16196253559e3487aa912ff770c09c7b08cdfbb21e9db1d8af1304ea7e0 ./Cargo.toml
37e255dd094e4720f96b74e501a179cf81c838ff327cbc1924b8fc4b5ea555fa ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
84a0803bc75dc3199e23ef56c11c6950e8bd888c686ecda7cc369829e8e3073f ./README.md
c1cac4ca532e73d64e5b090ad410a2161e9790b8d77fc6732adf52c248fb3472 ./README.md
29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs
604a795dec077dd7e4f3462a564fdda68e8084aef07da0fcf3dfb6053a46cc58 ./docs/API.md
0da3db314314e19a00c08700f94495075fecd1e54a5b188f6a62cb237307e96f ./docs/API.md
a881ba0808dd3f189d92038f6163c39d6b6208f6dbff5c32c0ea1a2628f3fbb4 ./docs/FLOW.md
8babaa6830fa70b2d3d8208ef51808e79c9d68c4e007b9ba1537fe582ba03b3d ./docs/openapi.json
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
0993076ca45b6d31859680835204cdc111b3f9bdbf460d50b4efec56cd5e685b ./ha-addon/Dockerfile
adf7ff248d782a3b1cba82b8789c57b6849ddaaaf5fbc108af75e18b1534a885 ./ha-addon/README.md
dd9291b12afade6fc20d66e80a5513251270e18e4c265260b2bbbe393a5a94c4 ./ha-addon/build.sh
4061846cec4d829e4101b4021df7f07dc2e0293ccfd11e91589f49733ec7c5b4 ./ha-addon/home-assistant/README.md
55ffdcc5b0a43d930823cea7c94235990dbbc4a35b60832b9adca1faf606d036 ./docs/openapi.json
77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example
0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile
0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md
a4596a70e50acfe698a8ae1dcb9bebd04adba95b69781218fca7add82c17703e ./ha-addon/build.sh
7dd2534a6f8405e640e28b7cb0e94270cc5f01fad8a972193ec254befe8a96b9 ./ha-addon/home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py
3f6ef15ef58456376ac53fde7cace1ef359d6a6f7a64c5b575ad77ea6e55ccc4 ./ha-addon/home-assistant/custom_components/gree_controller/api.py
da2a0a9390ab75cef150cd281bc329fa409d0d28f51382b36bd8de3a73325c16 ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
@@ -21,7 +20,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
d467809d894b8cb9e8e04a2cc279e0cec3227c1d1c689098bdc3c0380aa9043e ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
d1ec103c9a167382e33877183b6df221a5cced5db58cf5d6eefd2eb24ab16077 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -29,10 +28,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
e3a227ffe23368b152d0ba4a4e81a95b70d35e9870b2a8dc7b2048d6901a7f05 ./ha-addon/repository/gree-controller/CHANGELOG.md
1b5599195329cc3e643c23aa1caadcec80f133e8e515eec7d92b9f3cbda5b80b ./ha-addon/repository/gree-controller/DOCS.md
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
21b82216d0d84cc66ecbed69457caf61174fa9304528fe051c98e8ef7c634025 ./ha-addon/repository/gree-controller/config.yaml
1d1508e247b67487e6aac52b532adec2842494ec2e60144491c5144967a4f8f3 ./ha-addon/repository/gree-controller/CHANGELOG.md
50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md
e3834bf712d4cd2acc715d8c85db96dd6e2fb3fbb758dac537f6034c337b3745 ./ha-addon/repository/gree-controller/README.md
d060ee39cdea4de68bd9ededaebb7b81d974821e418b69e00c39cbdebd61f801 ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -84,7 +83,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
81f2d3f512f21d167c6d18bd2bda9d9e1557c0b234aa7826148ea557d726ddc2 ./scripts/api_dev_test.py
b9bd0f1db6bf879ca3778c4969dd56d3c35b7bdd137df517dbd2cd2e11856080 ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -97,9 +96,9 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
bd7e0455c6a1501f761f0d4c00ff74833eecbd75078af9168235f90a2d9485b3 ./src/api.rs
f4ace9a1104ab1d0016db1d34c3ac2c4010fc1a5534af5fbf02d2042d5815d8c ./src/api.rs
0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs
b24092f34a46626d64628b510eba82193f0b203fd097f6a621b91af5d91eb571 ./src/api/auth.rs
1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
@@ -109,16 +108,16 @@ ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/devi
2ef2f64b77886e2abd6b410adf8de91a2333d3ebdb8fcee8376ba566e5bba029 ./src/api/flows.rs
82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
3cc64b9c992805f3fbf01c0785cf29ce3121750bf30bd6420ed6e5cf2f8df422 ./src/api/history.rs
b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs
e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs
8495967c3df72110f591c744975984c78cafc8181b2d430e7fd5b9baf57eba95 ./src/api/integrations.rs
7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs
45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs
c01b8b942e64a46dac19ce08346aa844df7e9ac7147c0ad6d0f2cfd90e071ec0 ./src/api/openapi.rs
b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs
bb57f4b28df75ba350c9706719650fff3a689afee0cb6979c1fcae79396ac0cf ./src/api/system.rs
94d3845def693aafe15fa6e8b390881c46c3c236b62f247ddfdc055d9226ed22 ./src/api/websocket.rs
3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs
ddd1bfc8625b66f6e5f3d24b3122ac86c25f89b0af527e3dfce9102551a8f142 ./src/api/zones.rs
0b7d2ace2e8cb1dd0cb59aa6ca3e80cec14078e93530fd7b58081798585cd833 ./src/config.rs
de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs
@@ -155,12 +154,12 @@ af4edaf12faf294f44a8992d120a99b09edd1cbdd66981390e027d13fd6b6790 ./src/engine/t
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
fb169bcc20f95abe9892367df6dd3a683584bdbc721792d6ffb9311b81ca30d9 ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
c55d32f288fe95869fc288d47817b8e289fbe284be4219e81b1e7493ff5e3a57 ./src/home_assistant.rs
a9768150162a010579d40060417e276703e8756c139fcf57dddab9fffc98d840 ./src/home_assistant.rs
bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
2d36405912646b30b1a1a37a7a4e18c2104ab22e865b593316a609dda884aec2 ./src/influxdb/query.rs
67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs
1982ce54737a8780d1404b105d53db59cd5bee3d1c426f9a881d26cef7059bf5 ./src/main.rs
62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs
23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs
920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs
fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs
@@ -174,7 +173,7 @@ f1359b68f1d041cd441f80429e77893eeb6f6194613b58fd3e7a212fb511db3e ./src/models/r
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs
cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs
7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs
daca14e9a4dd65394add7c7753c8ae5e7e43e991323a5ca23d8788aae4428925 ./src/protocol/gree.rs
7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs
91d82c3ecae21615e4edad31b7ea95dca9b578650348666a2b61475b5a644669 ./src/protocol/gree/commands.rs
@@ -185,10 +184,10 @@ ff33db623f8f1bf30015b4a00e1f247a99de713da3d297c672e42c0795ec845c ./src/protocol
1076cb80950be00ca19f9e2370c73040902d1483b0e80c0ca349c7830d38c9f7 ./src/protocol/gree/polling.rs
e0555fac67d1dd0129ecfea6d1fd2fbeb1a73ac89ad95d1b74a7094e50e29ae4 ./src/protocol/gree/tests.rs
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
297983059c915057869fbb4f6bcfe752f3d5bd1e93c3a2651a1bf883c739ed29 ./src/protocol/gree_cloud.rs
b4e285f499dd787b75259777a14e3a51a1a071c7d176bcf30c1de0933920e0dc ./src/protocol/gree_cloud.rs
b4b1bad9cad889dd69681b2afc3e6a09ef19e19b2a1f2900fd417c4b1f55ec2c ./src/protocol/gree_cloud_mqtt.rs
6258ee3888a5f9a4f8bb6cfbbac97f2621de07f9da6bcb2fa596d452f47f2e49 ./src/protocol/mod.rs
2a08099b456847f24a828d711c4a0aba3fc372936d9a53f0be931f6706226cc5 ./src/provider.rs
a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
580917b91441fe9b3276cf0cfc31536ba7954b6a86f4080347b93f9367a5b1ac ./src/queries/entities.rs
+3 -3
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.15.2**.
HTTP and WebSocket API for GREE Controller **0.15.4**.
[← Main documentation](../README.md)
@@ -240,7 +240,7 @@ Response:
{
"status": "ok",
"name": "gree-controller",
"version": "0.15.2",
"version": "0.15.4",
"uptime_seconds": 1234,
"control_ready": true,
"time": "2026-08-30T06:54:00Z"
@@ -303,7 +303,7 @@ Returns the initial Web UI snapshot:
"control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
"control_plan_revision": 42,
"system": {
"version": "0.15.2",
"version": "0.15.4",
"uptime_seconds": 1234,
"auth_required": false,
"control_ready": true,
+4 -4
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -2,7 +2,7 @@
REGISTRY=repo.example.com
IMAGE_NAME=gree-controller
DISTRO=alpine
RUST_VERSION=1.89
RUST_VERSION=1.98.1
BUILD_PLATFORMS=linux/amd64,linux/arm64
BUILDER=gree-controller-multiarch
BUILDER_CONFIG=
+5 -3
View File
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1.7
ARG RUST_VERSION=1.89
ARG RUST_VERSION=1.98.1
ARG XX_VERSION=1.9.0
# Cross-compilation helpers. Builder stages always run on BUILDPLATFORM, so an
@@ -16,11 +16,12 @@ RUN apt-get update \
# Fetch Rust dependencies before TARGETPLATFORM enters the cache chain, so the
# same registry/git cache is reused by amd64 and arm64 builds.
COPY Cargo.toml Cargo.lock ./
COPY Cargo.toml ./
RUN --mount=type=cache,id=gree-controller-cargo-registry,target=/usr/local/cargo/registry \
--mount=type=cache,id=gree-controller-cargo-git,target=/usr/local/cargo/git \
mkdir -p src \
&& printf 'fn main() {}\n' > src/main.rs \
&& cargo generate-lockfile \
&& cargo fetch --locked \
&& rm -rf src
@@ -56,11 +57,12 @@ WORKDIR /src
RUN apk add --no-cache clang lld llvm file pkgconf
# Keep dependency fetching architecture-independent here as well.
COPY Cargo.toml Cargo.lock ./
COPY Cargo.toml ./
RUN --mount=type=cache,id=gree-controller-cargo-registry,target=/usr/local/cargo/registry \
--mount=type=cache,id=gree-controller-cargo-git,target=/usr/local/cargo/git \
mkdir -p src \
&& printf 'fn main() {}\n' > src/main.rs \
&& cargo generate-lockfile \
&& cargo fetch --locked \
&& rm -rf src
+4 -2
View File
@@ -2,6 +2,8 @@
This directory contains the OCI build, Home Assistant add-on repository source, custom integration and repository synchronization tooling.
The packaged controller includes its HTTP/WebSocket API and built-in Swagger documentation at `/api-docs` (`/api-docs/openapi.json` for OpenAPI 3.1).
## Configuration
Create the local build/repository configuration once:
@@ -20,7 +22,7 @@ Important variables:
REGISTRY=repo.example.com
IMAGE_NAME=gree-controller
DISTRO=alpine
RUST_VERSION=1.89
RUST_VERSION=1.98.1
BUILD_PLATFORMS=linux/amd64,linux/arm64
BUILDER=gree-controller-multiarch
BUILDER_CONFIG=
@@ -40,7 +42,7 @@ INTEGRATION_DOMAIN=gree_controller
./build.sh push
```
The project version comes only from root `Cargo.toml`. `render` synchronizes it to `Cargo.lock`, add-on `config.yaml` and the custom integration `manifest.json`, and sets the OCI image from `.env`.
The project version comes only from root `Cargo.toml`. `render` synchronizes it to add-on `config.yaml` and the custom integration `manifest.json`, and sets the OCI image from `.env`. The container build generates `Cargo.lock` from the exact direct dependency versions in `Cargo.toml` before fetching and compiling dependencies.
For the Home Assistant package, TCP `8787` is an intentional fixed host-network/ingress port contract. `build.sh render` verifies `config.yaml`, `run.sh`, the watchdog URL and Docker image metadata stay consistent. The runtime also compares the Supervisor-reported ingress port before starting. Standalone installations remain free to use any `GREE_CONTROLLER_BIND` port.
+6 -16
View File
@@ -32,7 +32,6 @@ VERSION="$(awk -F '"' '/^version = "/ {print $2; exit}' "$PROJECT_ROOT/Cargo.tom
IMAGE_REPO="${REGISTRY%/}/${IMAGE_NAME#/}"
IMAGE_TAG="${IMAGE_REPO}:${VERSION}"
CONFIG="$SCRIPT_DIR/repository/$ADDON_DIR/config.yaml"
LOCKFILE="$PROJECT_ROOT/Cargo.lock"
INTEGRATION_MANIFEST="$SCRIPT_DIR/home-assistant/custom_components/$INTEGRATION_DOMAIN/manifest.json"
REPOSITORY_CONFIG="$SCRIPT_DIR/repository/repository.yaml"
RUN_SCRIPT="$SCRIPT_DIR/run.sh"
@@ -44,18 +43,16 @@ DOCKERFILE="$SCRIPT_DIR/Dockerfile"
render_metadata() {
command -v python3 >/dev/null 2>&1 || { echo "python3 is required" >&2; exit 1; }
python3 - "$CONFIG" "$LOCKFILE" "$INTEGRATION_MANIFEST" "$REPOSITORY_CONFIG" "$VERSION" "$IMAGE_REPO" "$HA_REPO_URL" <<'PY'
python3 - "$CONFIG" "$INTEGRATION_MANIFEST" "$REPOSITORY_CONFIG" "$VERSION" "$IMAGE_REPO" "$HA_REPO_URL" <<'PY'
from pathlib import Path
import re
import sys
config = Path(sys.argv[1])
lockfile = Path(sys.argv[2])
manifest = Path(sys.argv[3])
repository = Path(sys.argv[4])
version = sys.argv[5]
image = sys.argv[6]
repo_url = sys.argv[7]
manifest = Path(sys.argv[2])
repository = Path(sys.argv[3])
version = sys.argv[4]
image = sys.argv[5]
repo_url = sys.argv[6]
lines = config.read_text(encoding="utf-8").splitlines()
out = []
@@ -70,13 +67,6 @@ for line in lines:
out.append(line)
config.write_text("\n".join(out) + "\n", encoding="utf-8")
lock = lockfile.read_text(encoding="utf-8")
pattern = r'(\[\[package\]\]\nname = "gree-controller"\nversion = ")[^"]+("\n)'
updated, count = re.subn(pattern, rf'\g<1>{version}\g<2>', lock, count=1)
if count != 1:
raise SystemExit("Could not find gree-controller package version in Cargo.lock")
lockfile.write_text(updated, encoding="utf-8")
import json
data = json.loads(manifest.read_text(encoding="utf-8"))
data["version"] = version
+2
View File
@@ -8,6 +8,8 @@ ha-addon/home-assistant/custom_components/gree_controller/
It creates HA entities for physical units, thermostat zones, whole-house controls and climate groups, while sending every command to the standalone Rust controller. Home Assistant therefore remains a client and UDP/AES GREE communication stays outside HA.
The controller exposes the API used by the integration and Web UI under `/api/*` and `/ws`; interactive Swagger documentation is available at `/api-docs`, with OpenAPI JSON at `/api-docs/openapi.json`.
The climate proxy supports power/turn on/off, HVAC modes, target temperature, fan mode, granular vertical louver positions and granular horizontal louver positions. `off`/`on` remain the first two swing modes for compatibility, followed by fixed positions and the supported vertical partial-swing ranges. The card UI translates those modes using the Home Assistant language while the underlying mode IDs (for example `fixed_upper`) remain unchanged for services, scripts and automations.
Automation plan:
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.15.2",
"version": "0.15.4",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
@@ -1,5 +1,18 @@
# Changelog
## 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.
- Migrates Axum route parameters and WebSocket text frames, Rand APIs, RustCrypto AES/AES-GCM APIs and Reqwest TLS configuration for the new dependency versions.
- Updates the Home Assistant add-on Rust builder to Rust 1.98.1.
- Pins direct Rust dependencies to exact updated releases; container builds generate a fresh lockfile before fetching and compiling with `--locked`.
- Uses `ring` as the single Rustls crypto provider across Reqwest and the direct MQTT TLS client, avoiding the Rustls provider ambiguity introduced by Reqwest 0.13.
## 0.15.3
- Simplifies the built-in Swagger API documentation: removes the embedded release-history changelog, starts with all endpoint groups collapsed and hides the global Schemas section.
- Documents the HTTP/WebSocket API, Swagger UI (`/api-docs`) and OpenAPI document (`/api-docs/openapi.json`) in the main documentation and Home Assistant add-on package.
## 0.15.2
- Adds a dedicated Visual Flow action for changing exactly one GREE unit function without modifying unrelated settings.
@@ -59,6 +59,10 @@ Broadcast zwykle nie przechodzi przez router. Sterowanie unicast może działać
| `public_chart_base_url` | opcjonalny bazowy URL publicznych linków Custom Chart; puste = automatyczne główne IPv4 hosta HA + `:8787` |
| `log_level` | `error`, `warn`, `info`, `debug`, `trace` |
### API i dokumentacja API
Aplikacja udostępnia HTTP API pod `/api/*` oraz WebSocket pod `/ws`. Interaktywna dokumentacja Swagger jest dostępna pod `/api-docs`, a dokument OpenAPI 3.1 pod `/api-docs/openapi.json`. Te same ścieżki działają przez ingress Home Assistant; bezpośredni dostęp na porcie `8787` podlega regułom `app_token`.
### Home Assistant API
Dodatek korzysta automatycznie z wewnętrznego proxy Home Assistant Core (`http://supervisor/core/api/`) i tokenu `SUPERVISOR_TOKEN` przekazywanego przez Supervisor. Gdy token jest wykryty, UI pokazuje automatyczną autoryzację i ukrywa ręczne pola URL/token. Dopiero nieudany `Test HA` odblokowuje awaryjny ręczny fallback. Token Supervisor jest używany tylko w pamięci procesu i nie jest zapisywany w SQLite.
@@ -125,6 +129,10 @@ Broadcast normally does not cross routers. Unicast control may work through rout
| `public_chart_base_url` | optional base URL for public Custom Chart links; empty = primary HA host IPv4 + `:8787` automatically |
| `log_level` | `error`, `warn`, `info`, `debug`, `trace` |
### API and API documentation
The application exposes its HTTP API under `/api/*` and WebSocket under `/ws`. Interactive Swagger documentation is available at `/api-docs`, with the OpenAPI 3.1 document at `/api-docs/openapi.json`. The same paths work through Home Assistant ingress; direct access on port `8787` follows the configured `app_token` rules.
### Home Assistant API
The add-on automatically uses the internal Home Assistant Core proxy (`http://supervisor/core/api/`) and the runtime `SUPERVISOR_TOKEN` provided by Supervisor. When the token is detected, the UI shows automatic authorization and hides manual URL/token fields. A failed `Test HA` unlocks the optional manual fallback. The Supervisor token is used only at runtime and is not stored in SQLite.
@@ -1,6 +1,6 @@
# GREE Controller
Local GREE HVAC controller packaged as a Home Assistant add-on, with Web UI, local API, UDP discovery/control, `amd64` and `aarch64` support, ingress, and database backup from `/data/gree-controller.db`.
Local GREE HVAC controller packaged as a Home Assistant add-on, with Web UI, HTTP/WebSocket API, built-in Swagger API documentation, UDP discovery/control, `amd64` and `aarch64` support, ingress, and database backup from `/data/gree-controller.db`.
![Home Assistant OS + GREE + VLAN topology](https://git.linuxiarz.pl/gru/gree-controller-ha-addon/raw/branch/master/gree-controller/topology-ha-vlan-gree-en.png)
@@ -8,6 +8,8 @@ For VLAN deployments, configure VLAN interfaces on the Home Assistant OS host. T
Full documentation: [DOCS.md](./DOCS.md).
The controller exposes `/api/*` and `/ws`. Interactive API documentation is available at `/api-docs`, with the OpenAPI 3.1 document at `/api-docs/openapi.json`. The same paths work through Home Assistant ingress; direct access on port `8787` follows the configured `app_token` rules.
The same Git repository also contains the optional `custom_components/gree_controller` integration. It is distributed with the repository but is not installed automatically with the add-on.
Repository: https://git.linuxiarz.pl/gru/gree-controller-ha-addon/
@@ -1,7 +1,7 @@
name: "GREE Controller"
version: "0.15.2"
version: "0.15.4"
slug: "gree_controller"
description: "Local GREE HVAC controller with Web UI 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/"
arch:
- amd64
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Developer smoke/integration tests for GREE Controller API 0.14.18.
"""Developer smoke/integration tests for GREE Controller API 0.15.4.
Default mode is read-only and safe to run against a real controller.
Use --settings-write to additionally round-trip all split settings resources and
@@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional
DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787")
DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "")
DEFAULT_EXPECTED_VERSION = "0.14.18"
DEFAULT_EXPECTED_VERSION = "0.15.4"
SETTINGS_PATHS: dict[str, set[str]] = {
"/api/settings/application": {"simulator_enabled"},
@@ -593,7 +593,7 @@ def run_suite(args: argparse.Namespace) -> int:
print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n")
runner.run("public health", lambda: test_health(client, args.expected_version))
runner.run("OpenAPI 0.14.18 contract", lambda: test_openapi(client, args.expected_version))
runner.run("OpenAPI 0.15.4 contract", lambda: test_openapi(client, args.expected_version))
runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client))
runner.run("protected API requires auth", lambda: test_protected_auth(client))
+28 -29
View File
@@ -34,7 +34,6 @@ use axum::{
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use chrono::{Duration as ChronoDuration, NaiveTime, Utc};
use futures_util::StreamExt;
use rand::{rngs::OsRng, RngCore};
use serde::Deserialize;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
@@ -91,31 +90,31 @@ pub fn router(state: AppState) -> Router {
.route("/api/discovery", post(discover))
.route("/api/devices", get(list_devices).post(add_device))
.route(
"/api/devices/:id",
"/api/devices/{id}",
get(get_device).patch(patch_device).delete(delete_device),
)
.route("/api/devices/:id/bind", post(bind_device))
.route("/api/devices/:id/poll", post(poll_device))
.route("/api/devices/:id/probe", post(probe_device))
.route("/api/devices/:id/command", post(command_device))
.route("/api/devices/{id}/bind", post(bind_device))
.route("/api/devices/{id}/poll", post(poll_device))
.route("/api/devices/{id}/probe", post(probe_device))
.route("/api/devices/{id}/command", post(command_device))
.route(
"/api/device-groups",
get(list_device_groups).post(create_device_group),
)
.route(
"/api/device-groups/:id",
"/api/device-groups/{id}",
get(get_device_group)
.put(update_device_group)
.delete(delete_device_group),
)
.route("/api/zones", get(list_zones).post(create_zone))
.route(
"/api/zones/:id",
"/api/zones/{id}",
get(get_zone).put(update_zone).delete(delete_zone),
)
.route("/api/zones/:id/control", post(update_zone_control))
.route("/api/zones/{id}/control", post(update_zone_control))
.route(
"/api/zones/:id/compressor-queue/cancel",
"/api/zones/{id}/compressor-queue/cancel",
post(cancel_zone_compressor_queue),
)
.route(
@@ -123,21 +122,21 @@ pub fn router(state: AppState) -> Router {
post(cancel_all_compressor_queues),
)
.route(
"/api/zones/:id/schedule-template",
"/api/zones/{id}/schedule-template",
post(apply_schedule_template),
)
.route("/api/groups", get(list_groups).post(create_group))
.route(
"/api/groups/:id",
"/api/groups/{id}",
get(get_group).put(update_group).delete(delete_group),
)
.route("/api/groups/:id/control", post(update_group_control))
.route("/api/groups/{id}/control", post(update_group_control))
.route("/api/house/control", post(update_house_control))
.route("/api/house/power", post(update_house_power))
.route("/api/house/preset", post(update_house_preset))
.route("/api/schedules", get(list_schedules).post(create_schedule))
.route(
"/api/schedules/:id",
"/api/schedules/{id}",
get(get_schedule)
.put(update_schedule)
.delete(delete_schedule),
@@ -147,7 +146,7 @@ pub fn router(state: AppState) -> Router {
get(list_automations).post(create_automation),
)
.route(
"/api/automations/:id",
"/api/automations/{id}",
get(get_automation)
.put(update_automation)
.delete(delete_automation),
@@ -155,10 +154,10 @@ pub fn router(state: AppState) -> Router {
.route("/api/flows", get(list_flows).post(create_flow))
.route("/api/flows/import", post(import_flow))
.route("/api/flows/simulate", post(simulate_flow))
.route("/api/flows/:id/export", get(export_flow))
.route("/api/flows/:id/logs", get(flow_logs))
.route("/api/flows/{id}/export", get(export_flow))
.route("/api/flows/{id}/logs", get(flow_logs))
.route(
"/api/flows/:id",
"/api/flows/{id}",
get(get_flow).put(update_flow).delete(delete_flow),
)
.route("/api/readings", get(readings))
@@ -215,7 +214,7 @@ pub fn router(state: AppState) -> Router {
get(list_access_tokens).post(create_access_token),
)
.route(
"/api/access-tokens/:id",
"/api/access-tokens/{id}",
axum::routing::delete(delete_access_token),
)
.route("/api/integrations/gree-cloud/test", post(test_gree_cloud))
@@ -224,7 +223,7 @@ pub fn router(state: AppState) -> Router {
get(discover_gree_cloud_devices),
)
.route(
"/api/integrations/gree-cloud/devices/:cloud_id/add",
"/api/integrations/gree-cloud/devices/{cloud_id}/add",
post(add_gree_cloud_device),
)
.route(
@@ -236,7 +235,7 @@ pub fn router(state: AppState) -> Router {
post(reconnect_gree_cloud),
)
.route(
"/api/devices/:id/cloud-diagnostics",
"/api/devices/{id}/cloud-diagnostics",
get(cloud_device_diagnostics),
)
.route(
@@ -271,7 +270,7 @@ pub fn router(state: AppState) -> Router {
get(list_devices),
)
.route(
"/api/integrations/home-assistant/devices/:id/command",
"/api/integrations/home-assistant/devices/{id}/command",
post(command_home_assistant_device),
)
.route(
@@ -283,7 +282,7 @@ pub fn router(state: AppState) -> Router {
get(list_home_assistant_groups),
)
.route(
"/api/integrations/home-assistant/groups/:id/control",
"/api/integrations/home-assistant/groups/{id}/control",
post(update_home_assistant_group_control),
)
.route(
@@ -299,7 +298,7 @@ pub fn router(state: AppState) -> Router {
post(update_house_power),
)
.route(
"/api/integrations/home-assistant/zones/:id/control",
"/api/integrations/home-assistant/zones/{id}/control",
post(update_home_assistant_zone_control),
)
.route_layer(middleware::from_fn_with_state(
@@ -309,8 +308,8 @@ pub fn router(state: AppState) -> Router {
let mut app = Router::new()
.route("/api/health", get(health))
.route("/api/public/charts/custom/:token", get(public_custom_chart))
.route("/charts/custom/:token", get(custom_chart_page))
.route("/api/public/charts/custom/{token}", get(public_custom_chart))
.route("/charts/custom/{token}", get(custom_chart_page))
.route("/custom-chart.js", get(custom_chart_js))
.route("/ws", get(websocket))
.route("/", get(index))
@@ -322,11 +321,11 @@ pub fn router(state: AppState) -> Router {
.route("/manifest.webmanifest", get(manifest))
.route("/sw.js", get(service_worker))
.route("/favicon.svg", get(favicon))
.route("/flows/:id", get(index))
.route("/flows/{id}", get(index))
.route("/lang/index.json", get(language_index))
.route("/lang/:file", get(language_file))
.route("/lang/{file}", get(language_file))
.route("/presets/index.json", get(preset_index))
.route("/presets/:file", get(preset_file))
.route("/presets/{file}", get(preset_file))
.merge(openapi::swagger_ui(&state.config.base_path))
.merge(protected)
.merge(home_assistant_api);
+1 -2
View File
@@ -99,7 +99,6 @@ fn hash_token(token: &str) -> String {
fn generate_access_token() -> String {
let mut bytes = [0u8; 32];
let mut rng = OsRng;
rng.fill_bytes(&mut bytes);
rand::fill(&mut bytes);
format!("gree_controller_{}", URL_SAFE_NO_PAD.encode(bytes))
}
+1 -2
View File
@@ -550,8 +550,7 @@ fn validate_public_chart_spec(series: &[String]) -> Result<(), AppError> {
fn generate_public_chart_token() -> String {
let mut bytes = [0u8; 32];
let mut rng = OsRng;
rng.fill_bytes(&mut bytes);
rand::fill(&mut bytes);
format!("chart_{}", URL_SAFE_NO_PAD.encode(bytes))
}
+2 -4
View File
@@ -12,10 +12,8 @@ pub(super) fn swagger_ui(base_path: &str) -> SwaggerUi {
.external_url_unchecked("/api-docs/openapi.json", document(base_path))
.config(
SwaggerConfig::new([docs_url])
.filter(true)
.try_it_out_enabled(true)
.display_request_duration(true)
.persist_authorization(true),
.doc_expansion("none")
.default_models_expand_depth(-1),
)
}
+3 -3
View File
@@ -47,7 +47,7 @@ async fn send_ws_bootstrap(state: &AppState, socket: &mut WebSocket) -> Result<O
),
};
socket
.send(Message::Text(message.to_string()))
.send(Message::Text(message.to_string().into()))
.await
.map_err(|_| ())?;
Ok(revision)
@@ -81,7 +81,7 @@ async fn websocket_loop(state: AppState, mut socket: WebSocket) {
match event {
Ok(event) => {
if let Ok(text) = serde_json::to_string(&event) {
if socket.send(Message::Text(text)).await.is_err() { break; }
if socket.send(Message::Text(text.into())).await.is_err() { break; }
}
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
@@ -105,7 +105,7 @@ async fn websocket_loop(state: AppState, mut socket: WebSocket) {
continue;
}
let text = control_plan_ws_message(snapshot.as_ref()).to_string();
if socket.send(Message::Text(text)).await.is_err() { break; }
if socket.send(Message::Text(text.into())).await.is_err() { break; }
last_control_plan_revision = Some(snapshot.revision);
}
}
+2 -2
View File
@@ -94,8 +94,8 @@ fn request_client(
reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.user_agent(concat!("gree-controller/", env!("CARGO_PKG_VERSION")))
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.tls_danger_accept_invalid_certs(true)
.tls_danger_accept_invalid_hostnames(true)
.build()
.context("cannot build Home Assistant HTTPS client")
}
+8
View File
@@ -36,6 +36,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() -> Result<()> {
install_rustls_crypto_provider();
let config = Config::load()?;
init_tracing();
@@ -191,6 +192,13 @@ async fn main() -> Result<()> {
Ok(())
}
fn install_rustls_crypto_provider() {
// reqwest is built with `rustls-no-provider` while tokio-rustls enables only
// `ring`. Install it once at process startup so every rustls consumer uses
// the same provider and rustls never has to guess between ring and AWS-LC.
let _ = tokio_rustls::rustls::crypto::ring::default_provider().install_default();
}
fn init_tracing() {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,tower_http=info".into());
+16 -11
View File
@@ -1,10 +1,10 @@
use aes::{
cipher::{generic_array::GenericArray, BlockDecrypt, BlockEncrypt, KeyInit},
Aes128,
cipher::{BlockCipherDecrypt, BlockCipherEncrypt, KeyInit},
Aes128, Block,
};
use aes_gcm::{
aead::{AeadInPlace, KeyInit as AeadKeyInit},
Aes128Gcm, Nonce,
aead::{consts::U12, AeadInOut, KeyInit as AeadKeyInit},
Aes128Gcm, Nonce, Tag,
};
use anyhow::{anyhow, bail, Context, Result};
use base64::{
@@ -62,7 +62,8 @@ pub fn encrypt_v1(key: &str, plaintext: &[u8]) -> Result<String> {
data.extend_from_slice(plaintext);
data.extend(std::iter::repeat(pad as u8).take(pad));
for block in data.chunks_exact_mut(16) {
cipher.encrypt_block(GenericArray::from_mut_slice(block));
let block: &mut Block = block.try_into().expect("AES block is 16 bytes");
cipher.encrypt_block(block);
}
Ok(STANDARD.encode(data))
}
@@ -77,7 +78,8 @@ pub fn decrypt_v1(key: &str, ciphertext_b64: &str) -> Result<Vec<u8>> {
bail!("invalid AES-ECB ciphertext length")
}
for block in data.chunks_exact_mut(16) {
cipher.decrypt_block(GenericArray::from_mut_slice(block));
let block: &mut Block = block.try_into().expect("AES block is 16 bytes");
cipher.decrypt_block(block);
}
let pad = *data.last().ok_or_else(|| anyhow!("empty plaintext"))? as usize;
let valid_padding = pad > 0
@@ -106,10 +108,10 @@ pub fn encrypt_v2(key: &str, plaintext: &[u8]) -> Result<V2Encrypted> {
let key = normalize_key(key)?;
let cipher = <Aes128Gcm as AeadKeyInit>::new_from_slice(&key)
.map_err(|_| anyhow!("invalid AES-GCM key"))?;
let nonce = Nonce::from_slice(&GCM_NONCE);
let nonce: Nonce<U12> = GCM_NONCE.into();
let mut buffer = plaintext.to_vec();
let tag = cipher
.encrypt_in_place_detached(nonce, GCM_AAD, &mut buffer)
.encrypt_inout_detached(&nonce, GCM_AAD, (&mut buffer[..]).into())
.map_err(|_| anyhow!("AES-GCM encryption failed"))?;
Ok(V2Encrypted {
ciphertext: STANDARD.encode(buffer),
@@ -130,10 +132,13 @@ pub fn decrypt_v2(key: &str, ciphertext_b64: &str, tag_b64: &str) -> Result<Vec<
let mut data = GREE_BASE64_DECODE
.decode(ciphertext_b64)
.context("invalid GCM ciphertext")?;
let nonce = Nonce::from_slice(&GCM_NONCE);
let tag = GenericArray::from_slice(&tag_bytes);
let nonce: Nonce<U12> = GCM_NONCE.into();
let tag: &Tag = tag_bytes
.as_slice()
.try_into()
.map_err(|_| anyhow!("invalid GCM tag length"))?;
cipher
.decrypt_in_place_detached(nonce, GCM_AAD, &mut data, tag)
.decrypt_inout_detached(&nonce, GCM_AAD, (&mut data[..]).into(), tag)
.map_err(|_| anyhow!("AES-GCM authentication failed"))?;
// A few modules append 0xff filler bytes to decrypted JSON.
data.retain(|byte| *byte != 0xff);
+4 -3
View File
@@ -1,7 +1,7 @@
use super::crypto::decrypt_v1;
use aes::{
cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit},
Aes128,
cipher::{BlockCipherEncrypt, KeyInit},
Aes128, Block,
};
use anyhow::{anyhow, bail, Context, Result};
use base64::{engine::general_purpose::STANDARD, Engine as _};
@@ -387,7 +387,8 @@ fn encrypt_cloud_api(plaintext: &[u8]) -> Result<String> {
data.extend_from_slice(plaintext);
data.extend(std::iter::repeat(pad as u8).take(pad));
for block in data.chunks_exact_mut(16) {
cipher.encrypt_block(GenericArray::from_mut_slice(block));
let block: &mut Block = block.try_into().expect("AES block is 16 bytes");
cipher.encrypt_block(block);
}
Ok(STANDARD.encode(data))
}
+12 -9
View File
@@ -14,7 +14,6 @@ use crate::{
};
use anyhow::{anyhow, bail, Context, Result};
use chrono::{DateTime, Utc};
use rand::Rng;
use serde::Serialize;
use serde_json::{json, Value};
use std::{
@@ -1072,8 +1071,7 @@ impl GreeCloudProvider {
} else {
(encrypt_v1(key, &plaintext)?, None)
};
let cid = rand::thread_rng()
.gen_range(1_000_000_000_u64..=9_999_999_999_u64)
let cid = rand::random_range(1_000_000_000_u64..=9_999_999_999_u64)
.to_string();
let request_kind = inner_payload
.get("t")
@@ -2047,7 +2045,7 @@ pub async fn cloud_reconnect_loop(
}
}
let base = (1_u64 << attempt.min(6)).min(60);
let jitter = rand::thread_rng().gen_range(0..=base.min(10));
let jitter = rand::random_range(0..=base.min(10));
sleep(Duration::from_secs(base + jitter)).await;
}
}
@@ -2056,6 +2054,11 @@ pub async fn cloud_reconnect_loop(
mod tests {
use super::*;
fn test_http_client() -> reqwest::Client {
crate::install_rustls_crypto_provider();
reqwest::Client::new()
}
#[test]
fn status_parser_rejects_malformed_column_count() {
assert!(properties_from_payload(&json!({"t":"dat","cols":["Pow"],"dat":[]})).is_err());
@@ -2151,7 +2154,7 @@ mod tests {
#[tokio::test]
async fn mqtt_payload_is_ignored_after_all_cloud_devices_are_removed() {
let provider = GreeCloudProvider::new(reqwest::Client::new());
let provider = GreeCloudProvider::new(test_http_client());
let result = provider
.handle_raw_message("status/stale-parent/device", b"not-json-anymore")
.await;
@@ -2160,7 +2163,7 @@ mod tests {
#[tokio::test]
async fn mqtt_payload_for_unknown_parent_still_errors_when_devices_are_registered() {
let provider = GreeCloudProvider::new(reqwest::Client::new());
let provider = GreeCloudProvider::new(test_http_client());
provider.inner.registered.write().await.insert(
"AABBCCDDEEFF".into(),
RegisteredDevice {
@@ -2190,7 +2193,7 @@ mod tests {
None,
Arc::new(AtomicBool::new(false)),
);
let providers = ProviderDispatcher::new(local, reqwest::Client::new());
let providers = ProviderDispatcher::new(local, test_http_client());
let mut device = Device::simulated_default();
device.connection_type = ConnectionType::Local;
assert_eq!(providers.provider_name(&device), "local");
@@ -2200,7 +2203,7 @@ mod tests {
#[tokio::test]
async fn cloud_provider_rejects_local_device_before_network_io() {
let provider = GreeCloudProvider::new(reqwest::Client::new());
let provider = GreeCloudProvider::new(test_http_client());
let device = Device::simulated_default();
let error = provider.register_device(&device).await.unwrap_err();
assert!(error.to_string().contains("Local device"));
@@ -2242,7 +2245,7 @@ mod tests {
#[tokio::test]
async fn cloud_command_order_matches_reference() {
let provider = GreeCloudProvider::new(reqwest::Client::new());
let provider = GreeCloudProvider::new(test_http_client());
let mut device = Device::simulated_default();
device.id = "cloud-test".into();
device.connection_type = ConnectionType::GreeCloud;