v0.8.7
This commit is contained in:
+8
-6
@@ -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
@@ -633,7 +633,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.8.6"
|
||||
version = "0.8.7"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user