This commit is contained in:
Mateusz Gruszczyński
2026-09-01 13:33:16 +02:00
parent dc20267787
commit 97c26c67b6
10 changed files with 67 additions and 46 deletions
Generated
+1 -1
View File
@@ -633,7 +633,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.9.1"
version = "0.9.2"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+16 -16
View File
@@ -1,11 +1,11 @@
b38e05f8a00969270280b0062ee129fccee39ae9c11cb614a6ad7b2b7ae2dfc7 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
d16e2c4867f9e651053bbbb080927134e7e72df96ca1b26f62f7818f0a1f7f09 ./Cargo.lock
c180bd9e6cdd1db88d195237ccce4e8a8bef44888468e0fb1afd3b5e5dbefb6f ./Cargo.toml
567a478d1e1ceb9fefba0f091cf4100a2953297d6e3cfb254f621d36faec3235 ./Cargo.lock
285e8936199d086103e73cdac7d0257429aa6b3c5a91c3be5d964eb6e6064f58 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
57ed2504d5924581030d74ca4899813532165d08f6317704ee2f2002d4cc8ad0 ./README.md
0ba2649e1746f3d2084501d7dd4b7610fe6f530b8c748769313fa515a04b2e81 ./README.md
ec0f495ee6a618bdec10b260ee7838f7a872d8c5b0aa91c546226c1ad818f601 ./build.rs
027a96e4254e45305f5f573d3562920d4f253cba32bcd2c96b15eaaae94b3cfd ./docs/API.md
4a41bc92108df6764af95979e035722d844918c4106dcc10d79b871da8aea2cb ./docs/API.md
a4571b7d31c96be8b3f540574444742f99a208fc0cdf27a14bebad70bc916ab3 ./home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
@@ -16,7 +16,7 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assista
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
01df8a32e3159ce761f395b859e4b00f0485c8cf8b37cfdc844dbdf979268bce ./home-assistant/custom_components/gree_controller/manifest.json
71d6215226168129fcd616cb1de0a9ab70aa2aa678269b2e7234802768ce9822 ./home-assistant/custom_components/gree_controller/manifest.json
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
@@ -39,6 +39,7 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
4c8b0d10d83d6130ed6e2703c233ac380b25a6c57fc23a29821f823df90331f9 ./src/api.rs
50b653f5ae1c9d24eb2e158fb4af713f5ccabe259c83053fa3cd2a310c7780da ./src/api/assets.rs
6c34b1294d76b0eba4c56bfd0bf69bdd502de0e40838104f6ddf2db4e922683d ./src/api/auth.rs
08a732a445f44d34c644109762ac389b58c96d0fe47b309ce34f4479d67c3712 ./src/api/automations.rs
@@ -55,9 +56,9 @@ d3ddc310a4033e400f521babec02ec06990092d7349c40cd271a3d8406f70f4a ./src/api/publ
678990ae03c96628927eac90ef0e84b4da4518c78fb5208a5a0980f96b0496ee ./src/api/settings.rs
74c156717b37e83d329f7c52ac622e02f690621544c4eb07c4502194db55e6e9 ./src/api/system.rs
681cccb8d09f4ad9c2e6467a4dbe2f9d991125775a5d2bfc9043daf940fecd29 ./src/api/websocket.rs
9d8ab056dbd2047fd00062e7252c3e9bb972c988a56de23daea61b8be871bd61 ./src/api/zones.rs
4c8b0d10d83d6130ed6e2703c233ac380b25a6c57fc23a29821f823df90331f9 ./src/api.rs
434f1e56112f76c931eba2df5556738d3a315547f6b918cba8322cee8c411af9 ./src/api/zones.rs
2b6eecead75cd4cdb98a9f74dd1c374dbf11ebfa716abbc3476aa12398af9acd ./src/config.rs
369455b5ffb125f1a80b767f62fd8c84a953adb69548dbd8adb653a6fbaaa45b ./src/db.rs
24580b279cfcd5388ba584eaba2c9bb729b8aae81e77b781c23fb804c5b5fa5d ./src/db/climate.rs
3de3fa4262be00029dde2344c4cfa41acd4acc0adec176f344761ff35ea730b6 ./src/db/configuration.rs
56b0efe84dbf6bd3ee2397b099220a68966311c187f4d5b53007a1693a5ef327 ./src/db/core_devices.rs
@@ -67,32 +68,32 @@ f3bd91d0bdb7699f319b9341d928d3cc3b25f759653fcd2095a5c5f695d08fac ./src/db/event
7e2fbce70f40aef360534ba38c6700163db77cb7288b3bf58728fce72a126435 ./src/db/schedules_automations.rs
35cbb521a8ec456756496b8aee71f9dfebad528a2cf2653ba8b48f6035be0c01 ./src/db/tests.rs
a384ea4042d1d11692b5f8693df1f54850dcc4e80726780b44b2b096bf667b78 ./src/db/zone_history.rs
369455b5ffb125f1a80b767f62fd8c84a953adb69548dbd8adb653a6fbaaa45b ./src/db.rs
952ea5906d39ef917ad71edc345f76b29c07c63079644fd5699b12aa254cb35f ./src/engine.rs
d418f27edde4a8a8ea89817899abfc48df632ec581deacf64db1dc409eef920f ./src/engine/automations.rs
910248f30ac9d697816458cd393c5f7d4bbdd3a12e48cb41cbb3ddb68a1ce395 ./src/engine/commands.rs
d91c50e68baa8f90a30fd94be1fda8cf548ed7386c1058f14f8d5b32625c0b25 ./src/engine/control_plan.rs
38f42c86c31048780ca414f2c328f363ef803a0edbc1e1ed85503909a10b69ae ./src/engine/deadlines.rs
de747652055f222da31b8f671ffdcbe94277ace5082c24b6b579d9bdaa6d16ff ./src/engine/groups.rs
18a7669318949e533f206d25cf7b63771d7f0f2c0b375a72ff78a2d921baaa61 ./src/engine/history.rs
dad8930c3d811f02cfeced3aa4b9a404fe273ba1b99539bb174b9430575f3e22 ./src/engine/local_thermostat.rs
e199628c4580e6a225e77d17acaaf8edc2a1ff0e3f91595234bba1f53e6450e1 ./src/engine/local_thermostat.rs
ea41d99d69a2b7593df7e243009211a65b227b9a88ffbf4a2badba88a49c01bc ./src/engine/ownership.rs
9c37adf514c96840fd6a5c715c579350fa86d6dcc0655a2aa97d1fd7c7e04299 ./src/engine/polling.rs
3d48608a065aa52a1fd8b9c59356a74d8216ecee3d47f8ff8ef7d3d47170c617 ./src/engine/runtime.rs
ffa72b93502eece187a96adb4ec94aec6048d4a6dc7af66e7c7af0931318787f ./src/engine/schedules.rs
e0d5347f73066404f9df9d211e059fd10cf054466ae35bdad6533a862b5a816a ./src/engine/targets.rs
a9188b588b2617ce1547a45d46141f7e5f1bb0027a48675dd86b274f807d0875 ./src/engine/temperature.rs
31b14b6412fced31aeae92401af2c4a0501063f78b4577224c5df291ad7d3df4 ./src/engine/temporary_thermostat.rs
3fe9825d9ff5f41c9f988489ebe606b4700322e553cbe6cae1bec7c82affd25c ./src/engine/tests.rs
c2fe371f9245ce299015f0b4e43f6625a207016abf73d2419acb6bd7437f045a ./src/engine/temporary_thermostat.rs
08a450ec80d7ac61814cd6afad5667214e0eac5c0f064d73fe527cbc6d647224 ./src/engine/tests.rs
6f4541bad148d172a44cd3ddd22b49dc97e4b755554a7a8820222169090e7022 ./src/engine/zone_actions.rs
856bd291229ae9d0b3f21fb7fd47058b7af5a18bd195e48829f8162533613472 ./src/engine/zone_control.rs
952ea5906d39ef917ad71edc345f76b29c07c63079644fd5699b12aa254cb35f ./src/engine.rs
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs
6f9ef85cd53ba030b477bc3c186f0f7e762674124210eabcb0686f2806eaada1 ./src/influxdb.rs
feb50ecd60b9a9fe9de1f7d8e1e26caab5435d59347468f6266a318f431bc3d3 ./src/influxdb/codec.rs
553b44ec0321e62480059bc2451fd34ec7aabe9fc87fc717daba0c4b55679d53 ./src/influxdb/query.rs
5e4d94f8a010df05bae9acab72c518080d2dba94d054df95b87e492305357222 ./src/influxdb/write.rs
6f9ef85cd53ba030b477bc3c186f0f7e762674124210eabcb0686f2806eaada1 ./src/influxdb.rs
c3ff7232a2fa1d29428872a10cfd72ee60a3a1faaa662eff6c2d8c7dffc1ed3a ./src/main.rs
e935e1fcf0d379b627df5d5055dbdad3dd55786fde7191376918b05afdde1727 ./src/models.rs
fca124ec30b5c9286f0f44cc18e8953bb2809f5009d4d521deaebfdeb00c1bd1 ./src/models/automation.rs
60ff96965cf9531b3964add783a6a45578aa1986844e99a239f182eb4f962d5b ./src/models/control_plan.rs
8228b9b6d9e8b1d13de29ce88500beacd88c8877893b7a3e41a4bd1e237ca36c ./src/models/defaults.rs
@@ -102,9 +103,9 @@ cfbaa853a5185c4be572588577b313a2d618837f7c07a3860d0e8d2cca04ee60 ./src/models/h
b1af7f8d4c3607b1c0ae978feaab4dfa03faa0154c7328712bb4eeafc27faa02 ./src/models/runtime.rs
e77f8cdcb5fc0e51cdbf2527a69585a2f6f9089e159d1e45ec4a48735ab0e304 ./src/models/temporary_thermostat.rs
0bdc68fc1b3d1fd4d1e1253bd48b5c149cfc9b87e2fedb13288b3ea8a096af3d ./src/models/zone.rs
e935e1fcf0d379b627df5d5055dbdad3dd55786fde7191376918b05afdde1727 ./src/models.rs
9c735d7c475f21f2f388c21be784376f7184f0bf18af1886544ef51585a3b290 ./src/notifications.rs
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
f1f765331469f4219a551414833c19b6de413114c22fdd0468e1f417659553c9 ./src/protocol/gree.rs
c935e441933ba8709432cc3ac33ed868043a89188d0f48ea6fbfe34b1f8936a3 ./src/protocol/gree/binding.rs
563ded1758b135eb523f1fa0b1bf55fe5430ef55935b301fd6d79cbb980e1820 ./src/protocol/gree/commands.rs
5d4328199e2285016fd1941e909e5c09f7cdf9fd21ab6538c1bf8bc2f4af3ba3 ./src/protocol/gree/core.rs
@@ -114,15 +115,14 @@ a96061fbbcadac6c2df2e9708a48dcf819b951eb147f32c7d0d285ac5ae2491b ./src/protocol
45d0cd0ea40b9255bd811362547f5a667dac6bc7324074726130baf60aa41d2b ./src/protocol/gree/polling.rs
35fc903404045129e3046b7997e941b88f98ceb42485cccc872c2e4aa530c75b ./src/protocol/gree/tests.rs
55c2d615e76c9e4b08c4067ea734e81d81915144e637f3b51c19bea4fc879137 ./src/protocol/gree/transport.rs
f1f765331469f4219a551414833c19b6de413114c22fdd0468e1f417659553c9 ./src/protocol/gree.rs
a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol/mod.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
cd5d0befe9f51a7067201a6eaa8d8ed72182d359e42cbdc67c80aa12d722654b ./src/queries/entities.rs
2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs
2b46a8cdc5efb5f930dd8ff7b9f6c0e720cbca096a51d9ae047711dd280fa3b8 ./src/queries/maintenance.rs
f688db6f440ee7b0129ccc22838de0ee0e0cf2b6376173b05f853ca2555c2c1e ./src/queries/schema.rs
f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
6ee23fcaa62231d15006db078d4f763d1b67d2365fcc2d736d8a7da8df5ea382 ./src/state.rs
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
de66cab9b650cb448814bfeda6d216e168dd3e6f07a1a438e4a9de9acbe6b798 ./web/404.html
+1 -1
View File
@@ -2,7 +2,7 @@
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.
**Current release: 0.9.1**
**Current release: 0.9.2**
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
+4 -4
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.9.1**.
HTTP and WebSocket API for GREE Controller **0.9.2**.
[← Main documentation](../README.md)
@@ -212,7 +212,7 @@ Response:
{
"status": "ok",
"name": "gree-controller",
"version": "0.9.1",
"version": "0.9.2",
"uptime_seconds": 1234,
"control_ready": true,
"time": "2026-08-30T06:54:00Z"
@@ -236,7 +236,7 @@ Returns the initial Web UI snapshot:
"settings": {},
"outdoor_temperature": null,
"system": {
"version": "0.9.1",
"version": "0.9.2",
"uptime_seconds": 1234,
"auth_required": false,
"control_ready": true,
@@ -669,7 +669,7 @@ All fields optional:
A custom setpoint creates the same temporary manual override for every member zone and normally expires at that zone's next schedule boundary. Returns a group control/result object including updated members/state.
Group `power` is a scoped bulk-power/control action. `power=false` immediately powers member units off and releases `group:*` ownership; members are stored as individually-off thermostats rather than being blocked by membership in an OFF group, so a user can turn a thermostat back on independently. `power=true` clears that scoped thermostat-OFF state and immediately re-runs group thermostat arbitration. Explicit Web/Home Assistant group power actions take over older member manual/temporary ownership; scheduled group automations still respect higher-priority manual/local ownership. Group mode/preset/setpoint changes are accepted only while the group is ON. Whole-house power remains independent and authoritative.
Group `power` is a scoped bulk-power/control action. `power=false` immediately powers member units off and releases `group:*` ownership; members are stored as individually-off thermostats rather than being blocked by membership in an OFF group. This group-created OFF is indefinite (no 15-minute local hand-back), so the regulator cannot restart the unit by itself; a user can still turn an individual thermostat back on independently. `power=true` clears that scoped thermostat-OFF state and immediately re-runs group thermostat arbitration. Explicit Web/Home Assistant group power actions take over older member manual/temporary ownership; scheduled group automations still respect higher-priority manual/local ownership. Group mode/preset/setpoint changes are accepted only while the group is ON. Whole-house power remains independent and authoritative.
---
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.9.1",
"version": "0.9.2",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
+10 -5
View File
@@ -257,6 +257,11 @@ async fn apply_zone_control_patch(state: &AppState, id: &str, patch: ZoneControl
// lock before the per-zone lock so a concurrent schedule edit cannot leave an override
// pointing at an obsolete boundary (and so lock order stays schedule -> zone -> device).
let _schedule_guard = state.lock_schedule_operation().await;
// A local thermostat click is an ownership transition, not merely a field update.
// Serialize it with the complete thermostat cycle so an already-running arbitration
// pass cannot reach an automatic ON after the user has committed local OFF. Lock order
// stays schedule -> cycle -> zone -> device, matching schedule mutation paths.
let _cycle_guard = state.lock_zone_control_cycle().await;
// Serialize quick-thermostat changes with the same device lock used by GREE polling and
// manual-takeover detection. Without this, a poll that started just before a Web/HA
// thermostat action could save an older zone snapshot afterwards and resurrect a false
@@ -642,11 +647,11 @@ async fn apply_zone_control_patch(state: &AppState, id: &str, patch: ZoneControl
if was_enabled && !zone.enabled {
power_off_zone_device(state, &zone, "zone.quick_disabled").await;
} else if patch.power == Some(false) {
if let Err(err) = engine::send_command(
state,
&zone.device_id,
DeviceCommand { power: Some(false), ..Default::default() },
).await {
// Local OFF is an explicit safety/user intent. Force one OFF frame instead of
// trusting the cached device.power flag; a stale cache must never let a running
// unit survive a thermostat OFF click. The cycle lock held by this handler keeps
// automatic arbitration out until the OFF marker and physical command are complete.
if let Err(err) = engine::force_power_off_device(state, &zone.device_id).await {
state.log("error", "zone.local_power_error", &err.to_string(), json!({
"zone_id": zone.id, "device_id": zone.device_id, "power": false
}));
+8 -5
View File
@@ -23,16 +23,19 @@ pub fn set_local_thermostat_power(zone: &mut Zone, power: bool, now: DateTime<Ut
changed
}
/// Re-arm a local-OFF hand-back after a temporary direct/manual device takeover.
/// The countdown must start from the moment that manual control ends, not from the
/// older OFF action that happened before the takeover.
/// Re-arm a timed local-OFF hand-back after a temporary direct/manual device takeover.
/// An OFF with no resume_at is intentionally indefinite (for example a group OFF that
/// physically powers members down while releasing group ownership) and must never gain
/// a 15-minute timer merely because another ownership mode ended.
fn rearm_local_thermostat_resume(zone: &mut Zone, now: DateTime<Utc>) -> bool {
if zone.local_thermostat_power != Some(false) { return false; }
if zone.local_thermostat_power != Some(false) || zone.local_thermostat_resume_at.is_none() { return false; }
set_local_thermostat_power(zone, false, now)
}
fn local_thermostat_handback_is_active(zone: &Zone) -> bool {
zone.local_thermostat_power == Some(false) && !zone.device_manual_override
zone.local_thermostat_power == Some(false)
&& zone.local_thermostat_resume_at.is_some()
&& !zone.device_manual_override
}
/// Clear only the ordinary local Quick Thermostat. Temporary Quick Thermostat state is
+4 -12
View File
@@ -362,19 +362,11 @@ async fn expire_local_thermostat_overrides(state: &AppState, zones: &mut [Zone],
// Do not let the old timer expire underneath someone who is actively controlling
// the unit. When that takeover ends and the device returns to OFF, the deadline is
// re-armed from that moment.
// Only an explicit timed local-OFF participates in automatic hand-back.
// local_thermostat_power=false with resume_at=None is a deliberate indefinite OFF
// (notably the state produced by group OFF) and must stay off until the user/group
// explicitly turns it back on or resumes automation.
if !local_thermostat_handback_is_active(zone) { continue; }
if zone.local_thermostat_resume_at.is_none() {
// Upgrade safety for a persisted 0.7.10/0.7.11 local-OFF state: old releases
// had no hand-back deadline, so start one from the first cycle after upgrade.
set_local_thermostat_power(zone, false, now.clone());
zone.updated_at = now.clone();
state.db.save_zone(zone)?;
state.broadcast("zone.updated", serde_json::to_value(&*zone)?);
state.log("info", "zone.local_thermostat_resume_scheduled", &format!("Local thermostat hand-back scheduled for {}", zone.name), json!({
"zone_id": zone.id, "device_id": zone.device_id, "delay_minutes": LOCAL_THERMOSTAT_RESUME_DELAY_MINUTES
}));
continue;
}
let expired = zone.local_thermostat_resume_at.as_ref().map(|at| at <= &now).unwrap_or(false);
if !expired { continue; }
reset_local_thermostat_override(zone);
+21
View File
@@ -439,6 +439,27 @@ mod tests {
assert_eq!(effective_zone_mode(&zone, "off"), "cool");
}
#[test]
fn indefinite_local_off_does_not_create_or_rearm_handback() {
let mut zone = test_zone("device");
zone.local_thermostat_power = Some(false);
zone.local_thermostat_resume_at = None;
assert!(!local_thermostat_handback_is_active(&zone));
assert!(!rearm_local_thermostat_resume(&mut zone, Utc::now()));
assert_eq!(zone.local_thermostat_power, Some(false));
assert!(zone.local_thermostat_resume_at.is_none());
}
#[test]
fn timed_local_off_remains_blocking_until_explicit_deadline() {
let mut zone = test_zone("device");
let now = Utc::now();
set_local_thermostat_power(&mut zone, false, now);
assert!(local_thermostat_handback_is_active(&zone));
assert_eq!(zone.local_thermostat_power, Some(false));
assert!(zone.local_thermostat_resume_at.is_some());
}
#[test]
fn local_thermostat_off_restarts_backend_handback_deadline() {
let mut zone = test_zone("device");