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.
|
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:
|
Main changes:
|
||||||
|
|
||||||
- separated ordinary Quick Thermostat handback state from Temporary Quick Thermostat lifecycle;
|
- 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;
|
- 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;
|
- 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;
|
- 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;
|
- active delayed/at Temporary sessions can be edited without accidentally rescheduling their historical start;
|
||||||
- Temporary schedule-boundary deadlines are refreshed after schedule edits;
|
- Temporary schedule-boundary deadlines are refreshed after schedule edits;
|
||||||
- conflicting same-cycle automations are resolved deterministically, and blocked automations do not consume cooldown;
|
- 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
|
## Validation executed in this environment
|
||||||
|
|
||||||
@@ -33,9 +34,10 @@ Main changes:
|
|||||||
- Python syntax compilation for Home Assistant integration files;
|
- Python syntax compilation for Home Assistant integration files;
|
||||||
- shell syntax checks for `scripts/*.sh`;
|
- shell syntax checks for `scripts/*.sh`;
|
||||||
- regenerated and verified `FILE_MANIFEST.sha256`;
|
- regenerated and verified `FILE_MANIFEST.sha256`;
|
||||||
- final ZIP integrity check.
|
- 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`.
|
- 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
|
## 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]]
|
[[package]]
|
||||||
name = "gree-controller"
|
name = "gree-controller"
|
||||||
version = "0.8.6"
|
version = "0.8.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gree-controller"
|
name = "gree-controller"
|
||||||
version = "0.8.6"
|
version = "0.8.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["GREE Controller contributors"]
|
authors = ["GREE Controller contributors"]
|
||||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||||
54984fc7a67eb87643d09dc80cd812cf4b7e7077116c9efd4bf97bea2be7e170 ./BUILD_REPORT.md
|
cdd0ec91713fa02bd4cda59eb4720baf52b71417d342087cd0edf9d5d28122e7 ./BUILD_REPORT.md
|
||||||
68101e8986367a641aa21fcb24d0c8c80dee1161636c7f4c833f0e918a6f3951 ./Cargo.lock
|
36dda1a51edcbc9a6ea582397bf4718fea6904e37d7bbe4e5306478cb913a05e ./Cargo.lock
|
||||||
3ce722a5e1989ba030288191913fa79ae62faf5353312a37dda8d12a00300343 ./Cargo.toml
|
b33dfe965c8217b5c0e22d72f911d37224b74f25fdb75768f2fac16797b40b1d ./Cargo.toml
|
||||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||||
dc05ed0028f6d276ba07acdf2fc8598fd0862eb4fbc433ad6fd14a1d15338f74 ./README.md
|
bd9b05e5363ec51875b1e13a322f51cfe81260bb7ff1148cca6785ff5692808c ./README.md
|
||||||
91c826e2f8c974bdb7b15e01e9b20487f06ff887247ecd9600f1263c11a61b96 ./build.rs
|
91c826e2f8c974bdb7b15e01e9b20487f06ff887247ecd9600f1263c11a61b96 ./build.rs
|
||||||
d458a3b1ed381c885d7c4b7e38f306f3e66838d27480551436f1e340995ea6e2 ./docs/API.md
|
d458a3b1ed381c885d7c4b7e38f306f3e66838d27480551436f1e340995ea6e2 ./docs/API.md
|
||||||
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
|
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
|
||||||
@@ -24,7 +24,7 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assista
|
|||||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
|
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/icon.png
|
||||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.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
|
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
|
||||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
||||||
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
|
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
|
||||||
@@ -47,15 +47,15 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
|
|||||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||||
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
|
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
|
||||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||||
ac98b91da7db1b6d880626d7a930b29d508e2d3c9171a019a264baf3a3b89757 ./src/api.rs
|
efd6541d9b044686fd6c456031a2cdec825f50e558255b5c0ce3e22ba09a1a66 ./src/api.rs
|
||||||
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
||||||
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
|
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
|
||||||
66b264aeac7865b17faa60ca6b3673e97b31f3e6f43fb6dc048eb527f383da91 ./src/engine.rs
|
a334fbc3b53731492287db1074ba64d1062d45212e6bc69c5694f5e4e09aa554 ./src/engine.rs
|
||||||
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
|
4b271b6fc365b1078c01d6178eb563841b2ecaed5d8639196f58e1312d2236fe ./src/error.rs
|
||||||
c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs
|
c6ff66da9ad08506f839ec56a869ead3abc824b5311e0ea9244517d11f7f4207 ./src/home_assistant.rs
|
||||||
190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs
|
190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs
|
||||||
c67212da6a2bd5c2933ece31586ca4048b484fda3105f23a6e06e881b079d129 ./src/main.rs
|
c67212da6a2bd5c2933ece31586ca4048b484fda3105f23a6e06e881b079d129 ./src/main.rs
|
||||||
ae5405f581832e8a26ae6e751bf570d0c471ffc86f372b8f492ff2825f103d6b ./src/models.rs
|
3c085cb91f514ffef78a68505cfda21ad9947aa91f7a3946ec66893b1e3ba042 ./src/models.rs
|
||||||
ea4bd7f20759101c2525600a1f342d2fc0946e2f0b3208b03cc5a415b6947c10 ./src/notifications.rs
|
ea4bd7f20759101c2525600a1f342d2fc0946e2f0b3208b03cc5a415b6947c10 ./src/notifications.rs
|
||||||
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
|
7fc31fbf8841a073a1544b8c7a6390f1a15b56087486ca0596a8418340fa232a ./src/protocol/crypto.rs
|
||||||
bc03d88e5476386747ff5e32f1788cdc1fcd4bef238ab66df2dada282c348c8f ./src/protocol/gree.rs
|
bc03d88e5476386747ff5e32f1788cdc1fcd4bef238ab66df2dada282c348c8f ./src/protocol/gree.rs
|
||||||
@@ -63,7 +63,7 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol
|
|||||||
6a1c0cab3eab80ecd254c5e486b1fba068523edb8d699c64054c17f67f4a31d0 ./src/queries.rs
|
6a1c0cab3eab80ecd254c5e486b1fba068523edb8d699c64054c17f67f4a31d0 ./src/queries.rs
|
||||||
2d69811db832c90ce06035ee29665205a04e4c514a21ecae9623991a9b14d825 ./src/state.rs
|
2d69811db832c90ce06035ee29665205a04e4c514a21ecae9623991a9b14d825 ./src/state.rs
|
||||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||||
eaef425b5e587e266e315d62c4e178af8b9b9ce16327181216627672a924d621 ./web/app.js
|
4ea78fda84eb3202c8e16b9ab5f43fb77036163ff8f170757eb5bf32b711eb37 ./web/app.js
|
||||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||||
ee64c273b334c2d0f9747408b6142a8d506021cbf2107178c2213ca4e4a17c8c ./web/index.html
|
ee64c273b334c2d0f9747408b6142a8d506021cbf2107178c2213ca4e4a17c8c ./web/index.html
|
||||||
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
|
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.
|
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
|
## 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:
|
On Debian, Ubuntu or an LXC container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
unzip gree-controller-v0.8.6.zip
|
unzip gree-controller-v0.8.7.zip
|
||||||
cd gree-controller
|
cd gree-controller
|
||||||
chmod +x scripts/*.sh
|
chmod +x scripts/*.sh
|
||||||
./scripts/dev.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
|
## 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`.
|
- 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",
|
"domain": "gree_controller",
|
||||||
"name": "GREE Controller",
|
"name": "GREE Controller",
|
||||||
"version": "0.8.6",
|
"version": "0.8.7",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
|
|||||||
@@ -874,7 +874,6 @@ async fn apply_zone_control_patch(state: &AppState, id: &str, patch: ZoneControl
|
|||||||
started_at,
|
started_at,
|
||||||
activated_at: if immediate_activation { Some(now.clone()) } else { activated_at.clone() },
|
activated_at: if immediate_activation { Some(now.clone()) } else { activated_at.clone() },
|
||||||
state: state_value.into(),
|
state: state_value.into(),
|
||||||
generation: existing_session.as_ref().map(|session| session.generation.saturating_add(1)).unwrap_or(1),
|
|
||||||
active_mode,
|
active_mode,
|
||||||
restore_zone_enabled,
|
restore_zone_enabled,
|
||||||
restore_local_thermostat_power: if editing_active {
|
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.
|
/// deliberately untouched; the two ownership mechanisms have independent cleanup paths.
|
||||||
pub fn reset_local_thermostat_override(zone: &mut Zone) -> bool {
|
pub fn reset_local_thermostat_override(zone: &mut Zone) -> bool {
|
||||||
let temporary_active = zone.temporary_quick_thermostat.as_ref()
|
let temporary_active = zone.temporary_quick_thermostat.as_ref()
|
||||||
.map(|session| session.activated_at.is_some()
|
.map(|session| session.activated_at.is_some())
|
||||||
|| (session.generation == 0 && session.start_kind == "now" && zone.local_thermostat_power == Some(true)))
|
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if temporary_active {
|
if temporary_active {
|
||||||
let Some(session) = zone.temporary_quick_thermostat.as_mut() else { return false; };
|
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 {
|
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; };
|
zone.temporary_quick_thermostat.as_ref()
|
||||||
if session.activated_at.as_ref().map(|at| at <= &now).unwrap_or(false) { return true; }
|
.and_then(|session| session.activated_at.as_ref())
|
||||||
// Legacy recovery only for old immediate sessions persisted before activated_at existed.
|
.map(|activated_at| activated_at <= &now)
|
||||||
// Fresh delay/at sessions must never inherit activity from an unrelated local Quick ON.
|
.unwrap_or(false)
|
||||||
session.generation == 0
|
|
||||||
&& session.start_kind == "now"
|
|
||||||
&& session.started_at <= now
|
|
||||||
&& zone.local_thermostat_power == Some(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn temporary_quick_thermostat_hard_deadline(session: &TemporaryQuickThermostat) -> Option<DateTime<Utc>> {
|
fn temporary_quick_thermostat_hard_deadline(session: &TemporaryQuickThermostat) -> Option<DateTime<Utc>> {
|
||||||
@@ -3273,7 +3268,6 @@ mod tests {
|
|||||||
started_at: now.clone(),
|
started_at: now.clone(),
|
||||||
activated_at: Some(now),
|
activated_at: Some(now),
|
||||||
state: "active".into(),
|
state: "active".into(),
|
||||||
generation: 1,
|
|
||||||
active_mode: Some("heat".into()),
|
active_mode: Some("heat".into()),
|
||||||
restore_zone_enabled: Some(true),
|
restore_zone_enabled: Some(true),
|
||||||
restore_local_thermostat_power: None,
|
restore_local_thermostat_power: None,
|
||||||
@@ -3371,17 +3365,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 now = Utc::now();
|
||||||
let mut zone = test_zone("device");
|
let mut zone = test_zone("device");
|
||||||
zone.local_thermostat_power = Some(true);
|
zone.local_thermostat_power = Some(true);
|
||||||
let mut session = temporary_session(now);
|
let mut session = temporary_session(now);
|
||||||
session.activated_at = None;
|
session.activated_at = None;
|
||||||
session.generation = 0;
|
session.state = "scheduled".into();
|
||||||
zone.temporary_quick_thermostat = Some(session);
|
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));
|
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());
|
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]
|
#[test]
|
||||||
fn temporary_setpoint_keeps_priority_over_active_schedule() {
|
fn temporary_setpoint_keeps_priority_over_active_schedule() {
|
||||||
let mut zone = test_zone("device");
|
let mut zone = test_zone("device");
|
||||||
|
|||||||
@@ -288,10 +288,6 @@ pub struct TemporaryQuickThermostat {
|
|||||||
/// Explicit lifecycle state: scheduled | waiting_master | paused_manual | active.
|
/// Explicit lifecycle state: scheduled | waiting_master | paused_manual | active.
|
||||||
#[serde(default = "default_temporary_state")]
|
#[serde(default = "default_temporary_state")]
|
||||||
pub state: String,
|
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
|
/// Heat/cool mode captured when ownership really starts. It keeps a temporary session
|
||||||
/// independent from later whole-house mode changes until hand-back.
|
/// independent from later whole-house mode changes until hand-back.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
+2
-11
@@ -87,12 +87,7 @@ function temporarySessionStatus(zone) {
|
|||||||
const safetyRemaining = secondsUntil(session.safety_expires_at);
|
const safetyRemaining = secondsUntil(session.safety_expires_at);
|
||||||
const hardRemaining = secondsUntil(session.expires_at);
|
const hardRemaining = secondsUntil(session.expires_at);
|
||||||
const startRemaining = secondsUntil(session.started_at);
|
const startRemaining = secondsUntil(session.started_at);
|
||||||
const legacyActive = !session.activated_at
|
const pending = !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 target = Number(session.temperature_target ?? zone.effective_setpoint ?? zone.setpoint);
|
const target = Number(session.temperature_target ?? zone.effective_setpoint ?? zone.setpoint);
|
||||||
const targetText = Number.isFinite(target) ? target.toFixed(1) : '--';
|
const targetText = Number.isFinite(target) ? target.toFixed(1) : '--';
|
||||||
const tolerance = Number(session.tolerance_c ?? 0.3).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.start_at.value = dateTimeLocalValue(new Date(Date.now() + 60 * 60 * 1000));
|
||||||
form.until.value = dateTimeLocalValue(new Date(Date.now() + 2 * 60 * 60 * 1000));
|
form.until.value = dateTimeLocalValue(new Date(Date.now() + 2 * 60 * 60 * 1000));
|
||||||
const session = zone.temporary_quick_thermostat;
|
const session = zone.temporary_quick_thermostat;
|
||||||
const activeSession = !!session?.activated_at || (!!session
|
const activeSession = !!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()));
|
|
||||||
form.dataset.activeSession = activeSession ? 'true' : 'false';
|
form.dataset.activeSession = activeSession ? 'true' : 'false';
|
||||||
if (session) {
|
if (session) {
|
||||||
form.start_kind.value = session.start_kind || (new Date(session.started_at).getTime() > Date.now() ? 'at' : 'now');
|
form.start_kind.value = session.start_kind || (new Date(session.started_at).getTime() > Date.now() ? 'at' : 'now');
|
||||||
|
|||||||
Reference in New Issue
Block a user