This commit is contained in:
Mateusz Gruszczyński
2026-08-28 13:09:11 +02:00
parent 7b5c9e4528
commit 9a5e4892f4
10 changed files with 38 additions and 68 deletions
+8 -6
View File
@@ -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.
Generated
+1 -1
View File
@@ -633,7 +633,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.8.6"
version = "0.8.7"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -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"
+9 -9
View File
@@ -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
+9 -2
View File
@@ -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`.
@@ -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",
-1
View File
@@ -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 {
+7 -32
View File
@@ -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<Utc>) -> 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<DateTime<Utc>> {
@@ -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");
-4
View File
@@ -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)]
+2 -11
View File
@@ -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');