v0.8.9
This commit is contained in:
+10
-38
@@ -1,43 +1,15 @@
|
|||||||
# GREE Controller v0.8.7 - audit remediation and cleanup report
|
# GREE Controller v0.8.9 - remote takeover diagnostics report
|
||||||
|
|
||||||
Source baseline: GREE Controller v0.8.5 audited on 2026-08-28.
|
Source baseline: GREE Controller v0.8.8.
|
||||||
|
|
||||||
## Implemented remediation
|
## Changes
|
||||||
|
|
||||||
This build addresses the ownership, safety, lifecycle and scheduling findings from the v0.8.5 control-logic audit, including the critical K1-K9 findings and high-priority H1-H14 findings.
|
- removed the v0.8.8 debounce/confirmation heuristic that required an unexpected post-command GREE state to survive two polls;
|
||||||
|
- restored the previous controller-settling rule: only values matching a recent controller command or its pre-command baseline are suppressed; any other changed control field is immediately eligible for manual/pilot takeover;
|
||||||
|
- added an explicit `device.remote_control_detected` event before a poll-detected takeover is persisted;
|
||||||
|
- diagnostic metadata contains raw and effective changed fields, compact before/after physical device state, recent controller commands and baselines, remaining settling-window time, and current zone/Temporary Quick Thermostat state;
|
||||||
|
- retained the v0.8.8 mobile modal layout fix.
|
||||||
|
|
||||||
Main changes:
|
## Validation
|
||||||
|
|
||||||
- separated ordinary Quick Thermostat handback state from Temporary Quick Thermostat lifecycle;
|
The Rust toolchain is not installed in this build environment, so `cargo test`/`cargo check` cannot be run here. Static source checks, JavaScript syntax checks, JSON parsing, shell syntax checks, manifest regeneration and ZIP integrity verification are performed before packaging. The target-host updater should run the full Rust test suite before replacing the service.
|
||||||
- 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;
|
|
||||||
- active Temporary sessions freeze their active mode/target and defer house/group mode/preset changes until handback;
|
|
||||||
- whole-house/group ON no longer sends a bare `power=true` when there is no valid effective Heat/Cool decision;
|
|
||||||
- generic Settings cannot mutate house master/mode outside the House transition APIs;
|
|
||||||
- configuration export/import strips ephemeral runtime ownership, safely stops detached devices, reconciles gates and repolls devices;
|
|
||||||
- direct climate automations for thermostat-managed devices now modify durable zone state instead of competing one-shot frames;
|
|
||||||
- schedule boundaries and time automations participate in the common next-deadline wakeup;
|
|
||||||
- continuous temperature hold requires fresh observations and is reset across controller restarts/manual takeover;
|
|
||||||
- disabling a device or zone now follows the safe shutdown/cleanup path;
|
|
||||||
- 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 `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
|
|
||||||
|
|
||||||
- JavaScript syntax checks for web application files;
|
|
||||||
- JSON parsing for language and Home Assistant manifest files;
|
|
||||||
- Python syntax compilation for Home Assistant integration files;
|
|
||||||
- shell syntax checks for `scripts/*.sh`;
|
|
||||||
- regenerated and verified `FILE_MANIFEST.sha256`;
|
|
||||||
- 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 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.7"
|
version = "0.8.9"
|
||||||
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.7"
|
version = "0.8.9"
|
||||||
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,12 +1,12 @@
|
|||||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||||
cdd0ec91713fa02bd4cda59eb4720baf52b71417d342087cd0edf9d5d28122e7 ./BUILD_REPORT.md
|
330ef304a321f8ada55e4c2953793d8cd6709d4c476522608bd91ccd26810059 ./BUILD_REPORT.md
|
||||||
36dda1a51edcbc9a6ea582397bf4718fea6904e37d7bbe4e5306478cb913a05e ./Cargo.lock
|
c0de04155e7751108a924de64923a64cbd8e0cddaf39f1b8af5b92cecda90f15 ./Cargo.lock
|
||||||
b33dfe965c8217b5c0e22d72f911d37224b74f25fdb75768f2fac16797b40b1d ./Cargo.toml
|
ec8ba278feb21789672828847085c3671fbe8af179efc9284ae12f3084760897 ./Cargo.toml
|
||||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||||
bd9b05e5363ec51875b1e13a322f51cfe81260bb7ff1148cca6785ff5692808c ./README.md
|
840e83950ea5fa9532f855d10a10acc05b42e11207a476ef3f07a8c940d12218 ./README.md
|
||||||
91c826e2f8c974bdb7b15e01e9b20487f06ff887247ecd9600f1263c11a61b96 ./build.rs
|
91c826e2f8c974bdb7b15e01e9b20487f06ff887247ecd9600f1263c11a61b96 ./build.rs
|
||||||
d458a3b1ed381c885d7c4b7e38f306f3e66838d27480551436f1e340995ea6e2 ./docs/API.md
|
6e2953807ea0d1f346f99adb527391806cb088afdbc62e721193be7253a3bc9c ./docs/API.md
|
||||||
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
|
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
|
||||||
7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md
|
7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md
|
||||||
10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md
|
10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.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
|
||||||
8e6705e95e2124b69761f02582f5bd03e0600e5a128ccc12684b81b44c4494b1 ./home-assistant/custom_components/gree_controller/manifest.json
|
b59c800f2abc81b9eef671eb5daa070bd1ce99edabfd91e78e09a8995ece2bba ./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
|
||||||
@@ -50,7 +50,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda
|
|||||||
efd6541d9b044686fd6c456031a2cdec825f50e558255b5c0ce3e22ba09a1a66 ./src/api.rs
|
efd6541d9b044686fd6c456031a2cdec825f50e558255b5c0ce3e22ba09a1a66 ./src/api.rs
|
||||||
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
||||||
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
|
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
|
||||||
a334fbc3b53731492287db1074ba64d1062d45212e6bc69c5694f5e4e09aa554 ./src/engine.rs
|
cb50fa07357ca110bf37038262d3ede008ef36682cdb99cc18e0e66755e6289f ./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
|
||||||
@@ -67,6 +67,6 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree
|
|||||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||||
ee64c273b334c2d0f9747408b6142a8d506021cbf2107178c2213ca4e4a17c8c ./web/index.html
|
ee64c273b334c2d0f9747408b6142a8d506021cbf2107178c2213ca4e4a17c8c ./web/index.html
|
||||||
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
|
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
|
||||||
6aa06f52e4c6391a43016a6a585b7422e3de319744db40a9b233beb27e57412d ./web/styles.css
|
c4e68eae3cfb5c44f5297eec2e97545d6e493d2e9fc7a0d4c2d7edc5bd609225 ./web/styles.css
|
||||||
bd2fe227d905b0cd5fb00ca9e92fb94c0d6c4402a9ae3ee51c68f6c1848fb67b ./web/sw.js
|
9d585effe4389fc307f8be8d656cf3eae4a96ad492a847c7acc8002ee52acbcb ./web/sw.js
|
||||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||||
|
|||||||
@@ -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.7**.
|
Current version: **0.8.9**.
|
||||||
|
|
||||||
## 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.7.zip
|
unzip gree-controller-v0.8.9.zip
|
||||||
cd gree-controller
|
cd gree-controller
|
||||||
chmod +x scripts/*.sh
|
chmod +x scripts/*.sh
|
||||||
./scripts/dev.sh
|
./scripts/dev.sh
|
||||||
@@ -480,6 +480,20 @@ Zones now use setpoint modulation, global seasonal house mode, cooling/heating C
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 0.8.9 Remote-takeover diagnostics
|
||||||
|
|
||||||
|
- Reverted the v0.8.8 two-poll confirmation heuristic for unexpected GREE states; unexpected states outside the known controller-command/baseline settling envelope are again acted on immediately.
|
||||||
|
- Added `device.remote_control_detected` diagnostics for poll-detected manual/pilot takeover, including raw/detected fields, before/after physical state, active controller settling commands/baselines and remaining settling-window time, plus current zone/Temporary Quick Thermostat ownership state.
|
||||||
|
- Retains the v0.8.8 narrow-screen Temporary Quick Thermostat modal layout fix.
|
||||||
|
|
||||||
|
## 0.8.8 Poll-takeover confirmation and mobile modal fit
|
||||||
|
|
||||||
|
- A single unexpected GREE poll snapshot inside the controller-command settling window no longer immediately becomes a physical/pilot takeover. The same unexpected physical state must persist into the next poll, so one transient mixed/out-of-order frame cannot break a scheduled Temporary Quick Thermostat start.
|
||||||
|
- Genuine direct/remote changes are still recognized after confirmation; deferred candidates are not lost when the next poll has no before/after delta.
|
||||||
|
- On narrow screens the Temporary Quick Thermostat **End and return to automation** button is placed below the active-session text and uses the full modal width.
|
||||||
|
- Rotated the PWA cache key so the corrected mobile stylesheet is fetched after upgrade.
|
||||||
|
|
||||||
## 0.8.7 Temporary-session state cleanup
|
## 0.8.7 Temporary-session state cleanup
|
||||||
|
|
||||||
- `activated_at` is now the only source of truth for Temporary Quick Thermostat ownership.
|
- `activated_at` is now the only source of truth for Temporary Quick Thermostat ownership.
|
||||||
|
|||||||
+1
-1
@@ -164,7 +164,7 @@ curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: appl
|
|||||||
|
|
||||||
### Physical/manual device takeover
|
### Physical/manual device takeover
|
||||||
|
|
||||||
The poller compares climate-relevant unit state with the last controller-known state. An external change of power, HVAC mode, target temperature or fan speed (for example from the IR remote) sets `device_manual_override=true` on the assigned zone. Controller-originated thermostat/group commands are correlated with a short-lived expected-state record when GREE status has not settled yet, so their delayed status transition is not mistaken for an external takeover. The zone continues sensor/history updates but thermostat modulation, schedules, groups and direct-device automations stop issuing corrective climate commands. The controller stores the pre-takeover climate state; if the user later returns the unit to that operational state, the takeover is cleared automatically and the stale **Resume automation** prompt disappears. When the pre-takeover state was OFF, switching the unit OFF again is sufficient even if the remote kept a different dormant target/mode internally. If that OFF state belongs to an active local quick-thermostat hand-back, its countdown is suspended while direct/manual takeover is active and is re-armed for a fresh 15 minutes when the unit returns to OFF. Otherwise the manual-device override expires at the next schedule transition when one exists; without a future transition it stays active until explicitly resumed. The known GREE standby normalization from Low fan back to Auto is ignored so it does not create a false takeover.
|
The poller compares climate-relevant unit state with the last controller-known state. An external change of power, HVAC mode, target temperature or fan speed (for example from the IR remote) sets `device_manual_override=true` on the assigned zone. Controller-originated thermostat/group commands are correlated with a short-lived expected-state record when GREE status has not settled yet, so their delayed status transition is not mistaken for an external takeover. During that settling window, one otherwise-unexpected poll snapshot is treated as transient and must persist into the following poll before manual takeover is declared. The zone continues sensor/history updates but thermostat modulation, schedules, groups and direct-device automations stop issuing corrective climate commands. The controller stores the pre-takeover climate state; if the user later returns the unit to that operational state, the takeover is cleared automatically and the stale **Resume automation** prompt disappears. When the pre-takeover state was OFF, switching the unit OFF again is sufficient even if the remote kept a different dormant target/mode internally. If that OFF state belongs to an active local quick-thermostat hand-back, its countdown is suspended while direct/manual takeover is active and is re-armed for a fresh 15 minutes when the unit returns to OFF. Otherwise the manual-device override expires at the next schedule transition when one exists; without a future transition it stays active until explicitly resumed. The known GREE standby normalization from Low fan back to Auto is ignored so it does not create a false takeover.
|
||||||
|
|
||||||
The technical `POST /api/devices/{id}/command` path uses the same takeover semantics for climate-relevant changes; light-only and unrelated feature changes do not suspend the thermostat. Local quick-thermostat power is deliberately separate from this state. An explicit quick-zone action (local power, target, mode, preset or enabled state) resumes thermostat ownership from a physical/direct takeover automatically. It can also be resumed directly:
|
The technical `POST /api/devices/{id}/command` path uses the same takeover semantics for climate-relevant changes; light-only and unrelated feature changes do not suspend the thermostat. Local quick-thermostat power is deliberately separate from this state. An explicit quick-zone action (local power, target, mode, preset or enabled state) resumes thermostat ownership from a physical/direct takeover automatically. It can also be resumed directly:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "gree_controller",
|
"domain": "gree_controller",
|
||||||
"name": "GREE Controller",
|
"name": "GREE Controller",
|
||||||
"version": "0.8.7",
|
"version": "0.8.9",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
|
|||||||
+68
-1
@@ -563,6 +563,49 @@ fn command_field_matches_device(command: &DeviceCommand, field: &str, device: &D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn device_control_snapshot(device: &Device) -> Value {
|
||||||
|
json!({
|
||||||
|
"power": device.power,
|
||||||
|
"mode": device.mode.clone(),
|
||||||
|
"target_temperature": device.target_temperature,
|
||||||
|
"fan_speed": device.fan_speed,
|
||||||
|
"quiet": device.quiet,
|
||||||
|
"sleep": device.sleep,
|
||||||
|
"turbo": device.turbo,
|
||||||
|
"swing_vertical": device.swing_vertical,
|
||||||
|
"swing_horizontal": device.swing_horizontal,
|
||||||
|
"online": device.online,
|
||||||
|
"communication_failures": device.communication_failures,
|
||||||
|
"last_seen": device.last_seen.clone(),
|
||||||
|
"updated_at": device.updated_at.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn controller_settling_diagnostics(state: &AppState, device_id: &str) -> Value {
|
||||||
|
let pending = state.pending_controller_commands.lock().await;
|
||||||
|
let Some(expected) = pending.get(device_id).cloned() else {
|
||||||
|
return json!({ "active": false, "reason": "none" });
|
||||||
|
};
|
||||||
|
let now = Instant::now();
|
||||||
|
if now > expected.expires_at {
|
||||||
|
let expired_by_ms = now.saturating_duration_since(expected.expires_at).as_millis().min(u64::MAX as u128) as u64;
|
||||||
|
return json!({
|
||||||
|
"active": false,
|
||||||
|
"reason": "expired",
|
||||||
|
"expired_by_ms": expired_by_ms,
|
||||||
|
"commands": expected.commands,
|
||||||
|
"baselines": expected.baselines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let remaining_ms = expected.expires_at.saturating_duration_since(now).as_millis().min(u64::MAX as u128) as u64;
|
||||||
|
json!({
|
||||||
|
"active": true,
|
||||||
|
"remaining_ms": remaining_ms,
|
||||||
|
"commands": expected.commands,
|
||||||
|
"baselines": expected.baselines,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn suppress_expected_controller_changes(
|
async fn suppress_expected_controller_changes(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
device: &Device,
|
device: &Device,
|
||||||
@@ -1279,10 +1322,16 @@ fn set_device_manual_override(state: &AppState, zone: &mut Zone, fields: Vec<Str
|
|||||||
async fn detect_external_device_control(state: &AppState, before: &Device, after: &Device) -> Result<(), AppError> {
|
async fn detect_external_device_control(state: &AppState, before: &Device, after: &Device) -> Result<(), AppError> {
|
||||||
if before.id != after.id { return Ok(()); }
|
if before.id != after.id { return Ok(()); }
|
||||||
for mut zone in state.db.list_zones()?.into_iter().filter(|zone| zone.device_id == after.id) {
|
for mut zone in state.db.list_zones()?.into_iter().filter(|zone| zone.device_id == after.id) {
|
||||||
|
let raw_fields = externally_changed_control_fields(before, after, &zone);
|
||||||
|
let controller_settling = if raw_fields.is_empty() {
|
||||||
|
json!({ "active": false, "reason": "no_changed_control_fields" })
|
||||||
|
} else {
|
||||||
|
controller_settling_diagnostics(state, &after.id).await
|
||||||
|
};
|
||||||
let fields = suppress_expected_controller_changes(
|
let fields = suppress_expected_controller_changes(
|
||||||
state,
|
state,
|
||||||
after,
|
after,
|
||||||
externally_changed_control_fields(before, after, &zone),
|
raw_fields.clone(),
|
||||||
).await;
|
).await;
|
||||||
if zone.device_manual_override && manual_override_matches_baseline(&zone, after) {
|
if zone.device_manual_override && manual_override_matches_baseline(&zone, after) {
|
||||||
persist_manual_override_clear(state, &mut zone, "gree_poll", true)?;
|
persist_manual_override_clear(state, &mut zone, "gree_poll", true)?;
|
||||||
@@ -1295,6 +1344,24 @@ async fn detect_external_device_control(state: &AppState, before: &Device, after
|
|||||||
persist_manual_override_clear(state, &mut zone, "gree_poll", false)?;
|
persist_manual_override_clear(state, &mut zone, "gree_poll", false)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
state.log("info", "device.remote_control_detected", &format!("External/pilot control detected for {}", zone.name), json!({
|
||||||
|
"zone_id": zone.id.clone(),
|
||||||
|
"device_id": zone.device_id.clone(),
|
||||||
|
"raw_fields": raw_fields,
|
||||||
|
"detected_fields": fields.clone(),
|
||||||
|
"before": device_control_snapshot(before),
|
||||||
|
"after": device_control_snapshot(after),
|
||||||
|
"controller_settling": controller_settling,
|
||||||
|
"source": "gree_poll",
|
||||||
|
"zone_state": {
|
||||||
|
"enabled": zone.enabled,
|
||||||
|
"control_owner": zone.control_owner.clone(),
|
||||||
|
"control_source": zone.control_source.clone(),
|
||||||
|
"demand": zone.demand,
|
||||||
|
"local_thermostat_power": zone.local_thermostat_power,
|
||||||
|
"temporary_quick_thermostat": zone.temporary_quick_thermostat.clone(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
set_device_manual_override(state, &mut zone, fields, "gree_poll", before)?;
|
set_device_manual_override(state, &mut zone, fields, "gree_poll", before)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -825,6 +825,8 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
|
|||||||
.quick-thermostat-control .list-card-head { align-items:center; }
|
.quick-thermostat-control .list-card-head { align-items:center; }
|
||||||
.quick-thermostat-control .zone-quick-actions { min-width:116px; }
|
.quick-thermostat-control .zone-quick-actions { min-width:116px; }
|
||||||
.temporary-presets { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
.temporary-presets { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
||||||
|
.temporary-active-summary { align-items:stretch; flex-direction:column; }
|
||||||
|
.temporary-active-summary .danger { width:100%; white-space:normal; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 0.6.9 dashboard density and fit fixes */
|
/* 0.6.9 dashboard density and fit fixes */
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'gree-controller-v085-temporary-condition-activation-fix';
|
const CACHE = 'gree-controller-v088-manual-takeover-confirmation';
|
||||||
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
|
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
|
||||||
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
|
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
|
||||||
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];
|
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];
|
||||||
|
|||||||
Reference in New Issue
Block a user