From 9a5e4892f439e039bcad40be690c4f42605b5cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 28 Aug 2026 13:09:11 +0200 Subject: [PATCH] v0.8.7 --- BUILD_REPORT.md | 14 ++++--- Cargo.lock | 2 +- Cargo.toml | 2 +- FILE_MANIFEST.sha256 | 18 ++++----- README.md | 11 +++++- .../gree_controller/manifest.json | 2 +- src/api.rs | 1 - src/engine.rs | 39 ++++--------------- src/models.rs | 4 -- web/app.js | 13 +------ 10 files changed, 38 insertions(+), 68 deletions(-) diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md index aea9173..1382825 100644 --- a/BUILD_REPORT.md +++ b/BUILD_REPORT.md @@ -1,4 +1,4 @@ -# GREE Controller v0.8.6 - audit remediation report +# GREE Controller v0.8.7 - audit remediation and cleanup report Source baseline: GREE Controller v0.8.5 audited on 2026-08-28. @@ -9,7 +9,7 @@ This build addresses the ownership, safety, lifecycle and scheduling findings fr Main changes: - separated ordinary Quick Thermostat handback state from Temporary Quick Thermostat lifecycle; -- made `activated_at` authoritative for fresh Temporary sessions, with legacy fallback only for generation 0 data; +- made `activated_at` the sole authoritative marker for Temporary-session ownership; - added explicit Temporary lifecycle states and preserved/restored the underlying Quick/manual state; - delayed capture of `restore_zone_enabled` until actual takeover for delayed/at sessions; - manual/direct device takeover now outranks Temporary completion and pauses condition/runtime accounting; @@ -24,7 +24,8 @@ Main changes: - active delayed/at Temporary sessions can be edited without accidentally rescheduling their historical start; - Temporary schedule-boundary deadlines are refreshed after schedule edits; - conflicting same-cycle automations are resolved deterministically, and blocked automations do not consume cooldown; -- UI exposes scheduled/waiting-master/paused-manual/active Temporary states and uses the same fresh-session activation rule as the backend. +- UI exposes scheduled/waiting-master/paused-manual/active Temporary states and uses the same `activated_at` ownership rule as the backend. +- removed the legacy Temporary-session `generation=0` / `local_thermostat_power` activation fallback and the compatibility-only model field/test path. ## Validation executed in this environment @@ -33,9 +34,10 @@ Main changes: - Python syntax compilation for Home Assistant integration files; - shell syntax checks for `scripts/*.sh`; - regenerated and verified `FILE_MANIFEST.sha256`; -- final ZIP integrity check. -- fixed compiler-reported Rust ownership error E0382 in `src/api.rs` by evaluating the `start_kind == "now"` predicate before moving `start_kind` into `TemporaryQuickThermostat`. +- final ZIP integrity check; +- verified that no Temporary-session references to `generation`, `generation=0`, or the removed local-power activation fallback remain in `src/` or `web/`; +- retained the compiler fix for Rust E0382 in `src/api.rs` by evaluating the `start_kind == "now"` predicate before moving `start_kind` into `TemporaryQuickThermostat`. ## Rust toolchain note -This environment does not contain `cargo`, `rustc`, `rustfmt` or `rust-analyzer`, so the Rust build and Rust test suite could not be executed here. The Rust changes were additionally checked for balanced delimiters and consistency of the modified model initializers, but a real `cargo test` remains required before production deployment. +This environment does not contain `cargo`, `rustc`, `rustfmt` or `rust-analyzer`, so the Rust build and Rust test suite for v0.8.7 could not be executed here. The modified Rust files were checked for balanced delimiters and all removed model-field references were verified absent. Run `bash scripts/update.sh` on the target host to execute the full Rust test suite before service replacement. diff --git a/Cargo.lock b/Cargo.lock index bec7134..cf0a55f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.8.6" +version = "0.8.7" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 4239fd8..285b4bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.8.6" +version = "0.8.7" edition = "2021" authors = ["GREE Controller contributors"] description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index f7d63e9..3827e28 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,10 +1,10 @@ d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -54984fc7a67eb87643d09dc80cd812cf4b7e7077116c9efd4bf97bea2be7e170 ./BUILD_REPORT.md -68101e8986367a641aa21fcb24d0c8c80dee1161636c7f4c833f0e918a6f3951 ./Cargo.lock -3ce722a5e1989ba030288191913fa79ae62faf5353312a37dda8d12a00300343 ./Cargo.toml +cdd0ec91713fa02bd4cda59eb4720baf52b71417d342087cd0edf9d5d28122e7 ./BUILD_REPORT.md +36dda1a51edcbc9a6ea582397bf4718fea6904e37d7bbe4e5306478cb913a05e ./Cargo.lock +b33dfe965c8217b5c0e22d72f911d37224b74f25fdb75768f2fac16797b40b1d ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -dc05ed0028f6d276ba07acdf2fc8598fd0862eb4fbc433ad6fd14a1d15338f74 ./README.md +bd9b05e5363ec51875b1e13a322f51cfe81260bb7ff1148cca6785ff5692808c ./README.md 91c826e2f8c974bdb7b15e01e9b20487f06ff887247ecd9600f1263c11a61b96 ./build.rs d458a3b1ed381c885d7c4b7e38f306f3e66838d27480551436f1e340995ea6e2 ./docs/API.md 234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md @@ -24,7 +24,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 -ebcbec00f0c26f2c47e0c1cb3f6fcd68711a509c6cb7e8e95650e08ad8dc1254 ./home-assistant/custom_components/gree_controller/manifest.json +8e6705e95e2124b69761f02582f5bd03e0600e5a128ccc12684b81b44c4494b1 ./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 @@ -47,15 +47,15 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh -ac98b91da7db1b6d880626d7a930b29d508e2d3c9171a019a264baf3a3b89757 ./src/api.rs +efd6541d9b044686fd6c456031a2cdec825f50e558255b5c0ce3e22ba09a1a66 ./src/api.rs 9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs 5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs -66b264aeac7865b17faa60ca6b3673e97b31f3e6f43fb6dc048eb527f383da91 ./src/engine.rs +a334fbc3b53731492287db1074ba64d1062d45212e6bc69c5694f5e4e09aa554 ./src/engine.rs 4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs 190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs c67212da6a2bd5c2933ece31586ca4048b484fda3105f23a6e06e881b079d129 ./src/main.rs -ae5405f581832e8a26ae6e751bf570d0c471ffc86f372b8f492ff2825f103d6b ./src/models.rs +3c085cb91f514ffef78a68505cfda21ad9947aa91f7a3946ec66893b1e3ba042 ./src/models.rs ea4bd7f20759101c2525600a1f342d2fc0946e2f0b3208b03cc5a415b6947c10 ./src/notifications.rs 7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs bc03d88e5476386747ff5e32f1788cdc1fcd4bef238ab66df2dada282c348c8f ./src/protocol/gree.rs @@ -63,7 +63,7 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol 6a1c0cab3eab80ecd254c5e486b1fba068523edb8d699c64054c17f67f4a31d0 ./src/queries.rs 2d69811db832c90ce06035ee29665205a04e4c514a21ecae9623991a9b14d825 ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service -eaef425b5e587e266e315d62c4e178af8b9b9ce16327181216627672a924d621 ./web/app.js +4ea78fda84eb3202c8e16b9ab5f43fb77036163ff8f170757eb5bf32b711eb37 ./web/app.js e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg ee64c273b334c2d0f9747408b6142a8d506021cbf2107178c2213ca4e4a17c8c ./web/index.html fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest diff --git a/README.md b/README.md index 5640e36..ad3994b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Standalone local GREE air-conditioner controller written in Rust. It runs on a regular Linux host or an LXC container and provides a mobile-first web interface without depending on the vendor cloud. -Current version: **0.8.6**. +Current version: **0.8.7**. ## Highlights @@ -47,7 +47,7 @@ See [`BUILD_REPORT.md`](BUILD_REPORT.md) for package validation details and [`do On Debian, Ubuntu or an LXC container: ```bash -unzip gree-controller-v0.8.6.zip +unzip gree-controller-v0.8.7.zip cd gree-controller chmod +x scripts/*.sh ./scripts/dev.sh @@ -480,6 +480,13 @@ Zones now use setpoint modulation, global seasonal house mode, cooling/heating C +## 0.8.7 Temporary-session state cleanup + +- `activated_at` is now the only source of truth for Temporary Quick Thermostat ownership. +- Removed the legacy `generation=0` / `local_thermostat_power` activation fallback from the backend and Web UI. +- Removed the Temporary-session `generation` compatibility field and its legacy-only test path. +- Kept ordinary Quick Thermostat state only as an independent lower-priority owner restored after Temporary hand-back. + ## 0.8.6 Control-logic audit remediation - Separates ordinary Quick Thermostat handback state from Temporary Quick Thermostat lifecycle and makes fresh-session activation depend on `activated_at`. diff --git a/home-assistant/custom_components/gree_controller/manifest.json b/home-assistant/custom_components/gree_controller/manifest.json index c66997f..74ab881 100644 --- a/home-assistant/custom_components/gree_controller/manifest.json +++ b/home-assistant/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.8.6", + "version": "0.8.7", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/src/api.rs b/src/api.rs index 08fea5c..8f7b061 100644 --- a/src/api.rs +++ b/src/api.rs @@ -874,7 +874,6 @@ async fn apply_zone_control_patch(state: &AppState, id: &str, patch: ZoneControl started_at, activated_at: if immediate_activation { Some(now.clone()) } else { activated_at.clone() }, state: state_value.into(), - generation: existing_session.as_ref().map(|session| session.generation.saturating_add(1)).unwrap_or(1), active_mode, restore_zone_enabled, restore_local_thermostat_power: if editing_active { diff --git a/src/engine.rs b/src/engine.rs index d05614a..0cdbd04 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -651,8 +651,7 @@ fn local_thermostat_handback_is_active(zone: &Zone) -> bool { /// deliberately untouched; the two ownership mechanisms have independent cleanup paths. pub fn reset_local_thermostat_override(zone: &mut Zone) -> bool { let temporary_active = zone.temporary_quick_thermostat.as_ref() - .map(|session| session.activated_at.is_some() - || (session.generation == 0 && session.start_kind == "now" && zone.local_thermostat_power == Some(true))) + .map(|session| session.activated_at.is_some()) .unwrap_or(false); if temporary_active { let Some(session) = zone.temporary_quick_thermostat.as_mut() else { return false; }; @@ -691,14 +690,10 @@ pub fn reset_local_thermostat_override(zone: &mut Zone) -> bool { } pub fn temporary_quick_thermostat_is_active(zone: &Zone, now: DateTime) -> bool { - let Some(session) = zone.temporary_quick_thermostat.as_ref() else { return false; }; - if session.activated_at.as_ref().map(|at| at <= &now).unwrap_or(false) { return true; } - // Legacy recovery only for old immediate sessions persisted before activated_at existed. - // Fresh delay/at sessions must never inherit activity from an unrelated local Quick ON. - session.generation == 0 - && session.start_kind == "now" - && session.started_at <= now - && zone.local_thermostat_power == Some(true) + zone.temporary_quick_thermostat.as_ref() + .and_then(|session| session.activated_at.as_ref()) + .map(|activated_at| activated_at <= &now) + .unwrap_or(false) } fn temporary_quick_thermostat_hard_deadline(session: &TemporaryQuickThermostat) -> Option> { @@ -3273,7 +3268,6 @@ mod tests { started_at: now.clone(), activated_at: Some(now), state: "active".into(), - generation: 1, active_mode: Some("heat".into()), restore_zone_enabled: Some(true), restore_local_thermostat_power: None, @@ -3371,17 +3365,15 @@ mod tests { } #[test] - fn legacy_immediate_session_can_use_local_power_fallback_but_fresh_session_cannot() { + fn temporary_session_without_activation_marker_is_never_active() { let now = Utc::now(); let mut zone = test_zone("device"); zone.local_thermostat_power = Some(true); let mut session = temporary_session(now); session.activated_at = None; - session.generation = 0; + session.state = "scheduled".into(); zone.temporary_quick_thermostat = Some(session); - assert!(temporary_quick_thermostat_is_active(&zone, now)); - zone.temporary_quick_thermostat.as_mut().unwrap().generation = 1; assert!(!temporary_quick_thermostat_is_active(&zone, now)); } @@ -3454,23 +3446,6 @@ mod tests { assert!(zone.temporary_quick_thermostat.as_ref().unwrap().condition_started_at.is_none()); } - #[test] - fn legacy_active_temporary_session_without_activated_at_still_evaluates_condition() { - let now = Utc::now(); - let mut zone = test_zone("device"); - zone.current_temperature = Some(23.0); - zone.local_thermostat_power = Some(true); - let mut session = temporary_session(now.clone()); - session.activated_at = None; - session.condition_started_at = Some(now.clone() - chrono::Duration::hours(1)); - zone.temporary_quick_thermostat = Some(session); - - assert_eq!( - evaluate_temporary_quick_thermostat_condition(&mut zone, now, Some(now), 10), - Some("temperature_stable".into()) - ); - } - #[test] fn temporary_setpoint_keeps_priority_over_active_schedule() { let mut zone = test_zone("device"); diff --git a/src/models.rs b/src/models.rs index c74e14b..22372f7 100644 --- a/src/models.rs +++ b/src/models.rs @@ -288,10 +288,6 @@ pub struct TemporaryQuickThermostat { /// Explicit lifecycle state: scheduled | waiting_master | paused_manual | active. #[serde(default = "default_temporary_state")] pub state: String, - /// Fresh sessions use generation >= 1. Generation 0 identifies legacy payloads that - /// predate explicit activated_at/state semantics and may use the compatibility fallback. - #[serde(default)] - pub generation: u64, /// Heat/cool mode captured when ownership really starts. It keeps a temporary session /// independent from later whole-house mode changes until hand-back. #[serde(default)] diff --git a/web/app.js b/web/app.js index bf54ebf..07a1cec 100644 --- a/web/app.js +++ b/web/app.js @@ -87,12 +87,7 @@ function temporarySessionStatus(zone) { const safetyRemaining = secondsUntil(session.safety_expires_at); const hardRemaining = secondsUntil(session.expires_at); const startRemaining = secondsUntil(session.started_at); - const legacyActive = !session.activated_at - && Number(session.generation ?? 0) === 0 - && (session.start_kind || 'now') === 'now' - && zone.local_thermostat_power === true - && (!session.started_at || new Date(session.started_at).getTime() <= Date.now()); - const pending = !session.activated_at && !legacyActive; + const pending = !session.activated_at; const target = Number(session.temperature_target ?? zone.effective_setpoint ?? zone.setpoint); const targetText = Number.isFinite(target) ? target.toFixed(1) : '--'; const tolerance = Number(session.tolerance_c ?? 0.3).toFixed(1); @@ -1448,11 +1443,7 @@ function populateTemporaryThermostat(id) { form.start_at.value = dateTimeLocalValue(new Date(Date.now() + 60 * 60 * 1000)); form.until.value = dateTimeLocalValue(new Date(Date.now() + 2 * 60 * 60 * 1000)); const session = zone.temporary_quick_thermostat; - const activeSession = !!session?.activated_at || (!!session - && Number(session.generation ?? 0) === 0 - && (session.start_kind || 'now') === 'now' - && zone.local_thermostat_power === true - && (!session.started_at || new Date(session.started_at).getTime() <= Date.now())); + const activeSession = !!session?.activated_at; form.dataset.activeSession = activeSession ? 'true' : 'false'; if (session) { form.start_kind.value = session.start_kind || (new Date(session.started_at).getTime() > Date.now() ? 'at' : 'now');