v0.7.12-hotfix
This commit is contained in:
@@ -4,6 +4,12 @@
|
||||
|
||||
Version 0.7.12 adds a timed hand-back for per-zone Quick Thermostat OFF while preserving the strict separation between local thermostat ownership and true physical/manual-device takeover. After 15 minutes the zone returns to the current group/schedule logic instead of restoring stale physical state.
|
||||
|
||||
### Backend hand-back correction
|
||||
|
||||
- Local thermostat ON/OFF now goes through one backend helper, so every new OFF action creates a fresh authoritative 15-minute deadline and ON always cancels it.
|
||||
- A physical/direct-device takeover temporarily suspends expiry of an existing local-OFF hand-back. Returning the unit to the previous OFF state re-arms the countdown from that return moment instead of continuing an old timer.
|
||||
- The Home Assistant coordinator keeps the exact `local_thermostat_resume_at` returned by the zone-control API in its short optimistic overlay, preventing a stale in-flight plan refresh from briefly restoring an older countdown.
|
||||
|
||||
## Implemented changes
|
||||
|
||||
- Night-mode settings moved out of application Settings into a dedicated **Night mode** page.
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
b2539a47c2594c4abfb54965c01a0bac18f6bcbd70ed3f546201819ee793e5e1 ./BUILD_REPORT.md
|
||||
3e44651c91c8a91c8b5de05b65d264d6a3065c60baaf95238acc1d33ffd1e2a1 ./BUILD_REPORT.md
|
||||
7e3367a68ba90e5e5139fde8842cf5d81ac87a154aeda44a8331be946207da7d ./Cargo.lock
|
||||
e97cb5f6ccf91a32b2c97560140f1ea0314b27d61039fbd03564ceacfab7fe31 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
8a7844b5774ded9e19917d27c509c891062b4e06eb79e811f1c382fd31121d37 ./README.md
|
||||
a2d518d230dedd59352c70dd609f44474eb159ca1fb5df32ea2cec37b54ae91c ./README.md
|
||||
f2c8eb28fedb26bb8f5636e1805e7a782cc472b87d882f6752d520f16216a248 ./build.rs
|
||||
61977236e655ede9be9fb3ac1342fca3ca5f466f41ded5385515bc5c86a974b9 ./docs/API.md
|
||||
3f774fbdc32de8f437d7cf9c1ceb2ae195c674d0f48b8891b25cf36d152ee2c4 ./docs/API.md
|
||||
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
|
||||
7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md
|
||||
10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md
|
||||
2ef5cb04cc74be3aab77a9d117b9040f186c73a7dcf24375f1cca2d73e428449 ./docs/PROJECT_SPEC.md
|
||||
7748b1230029439a405139750b2e0607b8e463bfd56ad86982394105069697c5 ./docs/REVERSE_PROXY.md
|
||||
28f1403d84b01b37bebb72548aa39a75bb14bdbb611a43bfa761319957cd6f39 ./docs/SECURITY.md
|
||||
f617d0b003a6786e4da144b12134451aba6641d91d4e9aa690d5c0009de3a83c ./home-assistant/README.md
|
||||
95e3c8c99ba439a86a2fb05cfd8ef00c75cb959a024a349a082da935b726f500 ./home-assistant/README.md
|
||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
|
||||
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
|
||||
3d0169ddabe6c664f71e76582995ac749c35214ca717654d296f4da8b909f2bf ./home-assistant/custom_components/gree_controller/climate.py
|
||||
5e4aef2143e81bedb5a15dd4be5c71b64a3ab448ec6d33a20851edd098e5f529 ./home-assistant/custom_components/gree_controller/config_flow.py
|
||||
b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./home-assistant/custom_components/gree_controller/const.py
|
||||
b065b61ef1adafe96aae3e47bff3e8b0bba57796fdfac1503669046beb1c0157 ./home-assistant/custom_components/gree_controller/coordinator.py
|
||||
ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assistant/custom_components/gree_controller/coordinator.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/logo.png
|
||||
@@ -46,10 +46,10 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
c5d7839844b823966f2dac3f9a89e2b323a9c349840cb8a0edc571f1b055ae31 ./src/api.rs
|
||||
25b97d7cdfe8c64bc20fe22d6dec1038c59740c4243d345986569d57e2418d55 ./src/api.rs
|
||||
32b1d0a7ac2060fd18fe7f1ca1d110ee45beaf67e81514d24d6bde3d36301387 ./src/config.rs
|
||||
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
|
||||
b2ca8bc21f80734873a56792bdced9c85f7cdb6cd1c00d03ab161a8dd042d288 ./src/engine.rs
|
||||
af5d1d79920a2923171c12b94a2f524c3eeff69873279d19a23d72c0d4ced506 ./src/engine.rs
|
||||
ae3b496749a3fd723b243d9bea92e5d76249f52814c80359ad9bac53abacb074 ./src/error.rs
|
||||
081f6909e95839c7da8c40b866af9de237e0540f3e65e1ce739ce70a6fa94846 ./src/home_assistant.rs
|
||||
190b0a33431539676e5dd7796698077f16c179d42eae4501ca96a91bf797cbf8 ./src/influxdb.rs
|
||||
|
||||
@@ -483,7 +483,8 @@ Zones now use setpoint modulation, global seasonal house mode, cooling/heating C
|
||||
- Switching a single Quick Thermostat **OFF** now starts a 15-minute local-OFF window instead of leaving the zone outside automation indefinitely.
|
||||
- The Web UI shows a live `MM:SS` countdown and a **Resume now** action during that window.
|
||||
- At expiry, only local quick-thermostat ownership and its temporary quick profile/setpoint are cleared. The controller does not restore a stale physical ON/OFF state; it recalculates the current group, house mode, schedule, profile, room temperature and hysteresis.
|
||||
- Switching the local thermostat back **ON** cancels the pending hand-back. A physical/remote `device_manual_override` remains separate and is not cleared by the local timer.
|
||||
- Every fresh local **OFF** action creates a new backend-owned 15-minute deadline; switching the local thermostat back **ON** cancels it.
|
||||
- A physical/direct `device_manual_override` remains separate and temporarily suspends expiry of an existing local-OFF hand-back. Returning the unit to its previous OFF state re-arms a fresh 15-minute countdown from that moment.
|
||||
- Persisted local-OFF state from 0.7.10/0.7.11 receives a fresh 15-minute hand-back deadline on the first controller cycle after upgrade.
|
||||
|
||||
## 0.7.11 Cleanup and Rust test fix
|
||||
|
||||
+2
-2
@@ -137,11 +137,11 @@ curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: appl
|
||||
|
||||
`mode` on the quick zone endpoint accepts `house`, `cool`, or `heat`. `house` means **Follow global mode** in the UI and should be left unchanged when the zone is meant to inherit the global GREE Controller mode. A fixed `cool`/`heat` is an explicit per-zone override; therefore house mode `off` stops inherited zones but does not disable a zone deliberately fixed to Cooling or Heating. Whole-house master power remains authoritative over all zones.
|
||||
|
||||
`power:true/false` on `/api/zones/{id}/control` is a local quick-thermostat power override, not a physical/manual command. `true` lets that zone run through the full thermostat even when its climate group is off and cancels any pending local hand-back timer. `false` turns only that zone off immediately and sets `local_thermostat_resume_at` to 15 minutes in the future. When that deadline is reached, the controller clears local quick ownership plus its temporary quick preset/setpoint and recalculates the current group/schedule/house-mode state instead of restoring an old physical ON/OFF snapshot. `clear_local_thermostat_override:true` performs that hand-back immediately. Direct/pilot-style control remains `/api/devices/{id}/command`.
|
||||
`power:true/false` on `/api/zones/{id}/control` is a local quick-thermostat power override, not a physical/manual command. `true` lets that zone run through the full thermostat even when its climate group is off and cancels any pending local hand-back timer. Every accepted `false` action turns only that zone off immediately and creates a fresh backend-owned `local_thermostat_resume_at` 15 minutes in the future. When that deadline is reached, the controller clears local quick ownership plus its temporary quick preset/setpoint and recalculates the current group/schedule/house-mode state instead of restoring an old physical ON/OFF snapshot. `clear_local_thermostat_override:true` performs that hand-back immediately. Direct/pilot-style control remains `/api/devices/{id}/command`.
|
||||
|
||||
### 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. Otherwise the 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. 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:
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ The recommended `combined` strategy keeps the GREE sensor as the primary input a
|
||||
|
||||
Each controller zone is exposed as a full Home Assistant `climate` entity with current/target temperature and HVAC modes: Off, Auto (follow the controller house mode), Cool and Heat. For a **zone thermostat**, Auto does not mean the GREE unit's native automatic heat/cool algorithm: it means **inherit the whole-house Heating/Cooling selection from GREE Controller**. Direct physical-device climate entities still use the native GREE Auto mode. The target temperature remains published while a zone is Off, so Home Assistant can display the configured setpoint instead of `unknown`. The existing zone target `number` and enabled `switch` remain available for compatibility.
|
||||
|
||||
From version 0.7.9, each zone climate also supports preset modes `auto`, `comfort`, `sleep` and `away`, and the same choices are exposed as a separate **Work profile** `select` on the zone device. `auto` removes the temporary per-zone profile override and returns the zone to its schedule. The zone enable switch now reports the configured zone state independently from group power gates; `effective_enabled` remains available in attributes/sensors to show when an enabled zone is currently blocked by a disabled group. From version 0.7.10, climate entity ON/OFF controls local thermostat power rather than technically enabling/disabling the zone: local ON may run the zone while its group is off, with full thermostat logic intact. From 0.7.12, local OFF keeps that zone off for 15 minutes and then automatically returns ownership to the current group/schedule logic; the deadline is exposed as `local_thermostat_resume_at`. The separate zone Enabled switch remains the technical availability switch.
|
||||
From version 0.7.9, each zone climate also supports preset modes `auto`, `comfort`, `sleep` and `away`, and the same choices are exposed as a separate **Work profile** `select` on the zone device. `auto` removes the temporary per-zone profile override and returns the zone to its schedule. The zone enable switch now reports the configured zone state independently from group power gates; `effective_enabled` remains available in attributes/sensors to show when an enabled zone is currently blocked by a disabled group. From version 0.7.10, climate entity ON/OFF controls local thermostat power rather than technically enabling/disabling the zone: local ON may run the zone while its group is off, with full thermostat logic intact. From 0.7.12, every local OFF creates a fresh backend-owned 15-minute hand-back deadline exposed as `local_thermostat_resume_at`. A direct/pilot takeover suspends expiry of that local timer; if the unit is returned to the previous OFF state, the backend re-arms a fresh 15-minute countdown from that moment. The separate zone Enabled switch remains the technical availability switch.
|
||||
|
||||
From version 0.6.4, zone climate entities use `climate.<zone_name>_thermostat`, for example `climate.igor_thermostat`. On integration reload, existing zone climate registry entries are migrated to this scheme using the zone's current name. If the target entity ID is already occupied, the old ID is retained and Home Assistant logs a warning.
|
||||
|
||||
|
||||
@@ -136,12 +136,23 @@ class GreeControllerCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
self.async_set_updated_data(dict(self.data or {}))
|
||||
|
||||
try:
|
||||
await self.client.zone_control(zone_id, payload)
|
||||
zone = await self.client.zone_control(zone_id, payload)
|
||||
except GreeControllerApiError:
|
||||
self._pending_zone_controls.pop(zone_id, None)
|
||||
await self.async_request_refresh()
|
||||
raise
|
||||
|
||||
# The backend owns hand-back timing. Keep the exact accepted deadline returned
|
||||
# by the zone API in the optimistic overlay so an overlapping/stale plan poll
|
||||
# cannot temporarily resurrect the previous countdown in Home Assistant.
|
||||
for key in (
|
||||
"local_thermostat_power",
|
||||
"local_thermostat_resume_at",
|
||||
"device_manual_override",
|
||||
"device_manual_override_until",
|
||||
):
|
||||
if key in zone:
|
||||
expected[key] = zone[key]
|
||||
self._pending_zone_controls[zone_id] = (loop.time() + 3.0, expected)
|
||||
await self.async_request_refresh()
|
||||
|
||||
|
||||
+1
-6
@@ -622,12 +622,7 @@ async fn apply_zone_control_patch(state: &AppState, id: &str, patch: ZoneControl
|
||||
engine::reset_local_thermostat_override(&mut zone);
|
||||
}
|
||||
if let Some(power) = patch.power {
|
||||
zone.local_thermostat_power = Some(power);
|
||||
zone.local_thermostat_resume_at = if power {
|
||||
None
|
||||
} else {
|
||||
Some(Utc::now() + chrono::Duration::minutes(engine::LOCAL_THERMOSTAT_RESUME_DELAY_MINUTES))
|
||||
};
|
||||
engine::set_local_thermostat_power(&mut zone, power, Utc::now());
|
||||
if power { zone.enabled = true; }
|
||||
// A manually started local thermostat keeps an already selected target/profile until
|
||||
// it is switched off and the delayed hand-back completes, Auto is selected, or the
|
||||
|
||||
+76
-7
@@ -573,6 +573,34 @@ fn externally_changed_control_fields(before: &Device, after: &Device, zone: &Zon
|
||||
|
||||
pub const LOCAL_THERMOSTAT_RESUME_DELAY_MINUTES: i64 = 15;
|
||||
|
||||
/// Apply local quick-thermostat power ownership and keep the automatic hand-back
|
||||
/// deadline in one backend-owned place. Every fresh OFF action receives a fresh
|
||||
/// deadline; ON cancels any pending hand-back.
|
||||
pub fn set_local_thermostat_power(zone: &mut Zone, power: bool, now: DateTime<Utc>) -> bool {
|
||||
let resume_at = if power {
|
||||
None
|
||||
} else {
|
||||
Some(now + chrono::Duration::minutes(LOCAL_THERMOSTAT_RESUME_DELAY_MINUTES))
|
||||
};
|
||||
let changed = zone.local_thermostat_power != Some(power)
|
||||
|| zone.local_thermostat_resume_at != resume_at;
|
||||
zone.local_thermostat_power = Some(power);
|
||||
zone.local_thermostat_resume_at = resume_at;
|
||||
changed
|
||||
}
|
||||
|
||||
/// Re-arm a local-OFF hand-back after a temporary direct/manual device takeover.
|
||||
/// The countdown must start from the moment that manual control ends, not from the
|
||||
/// older OFF action that happened before the takeover.
|
||||
fn rearm_local_thermostat_resume(zone: &mut Zone, now: DateTime<Utc>) -> bool {
|
||||
if zone.local_thermostat_power != Some(false) { return false; }
|
||||
set_local_thermostat_power(zone, false, now)
|
||||
}
|
||||
|
||||
fn local_thermostat_handback_is_active(zone: &Zone) -> bool {
|
||||
zone.local_thermostat_power == Some(false) && !zone.device_manual_override
|
||||
}
|
||||
|
||||
pub fn reset_local_thermostat_override(zone: &mut Zone) -> bool {
|
||||
let changed = zone.local_thermostat_power.is_some()
|
||||
|| zone.local_thermostat_resume_at.is_some()
|
||||
@@ -590,10 +618,15 @@ pub fn reset_local_thermostat_override(zone: &mut Zone) -> bool {
|
||||
fn expire_local_thermostat_overrides(state: &AppState, zones: &mut [Zone], schedules: &[Schedule], house_mode: &str) -> Result<(), AppError> {
|
||||
let now = Utc::now();
|
||||
for zone in zones.iter_mut() {
|
||||
if zone.local_thermostat_power == Some(false) && zone.local_thermostat_resume_at.is_none() {
|
||||
// A direct device/pilot takeover has higher priority than the local-OFF hand-back.
|
||||
// Do not let the old timer expire underneath someone who is actively controlling
|
||||
// the unit. When that takeover ends and the device returns to OFF, the deadline is
|
||||
// re-armed from that moment.
|
||||
if !local_thermostat_handback_is_active(zone) { continue; }
|
||||
if zone.local_thermostat_resume_at.is_none() {
|
||||
// Upgrade safety for a persisted 0.7.10/0.7.11 local-OFF state: old releases
|
||||
// had no hand-back deadline, so start one from the first cycle after upgrade.
|
||||
zone.local_thermostat_resume_at = Some(now.clone() + chrono::Duration::minutes(LOCAL_THERMOSTAT_RESUME_DELAY_MINUTES));
|
||||
set_local_thermostat_power(zone, false, now.clone());
|
||||
zone.updated_at = now.clone();
|
||||
state.db.save_zone(zone)?;
|
||||
state.broadcast("zone.updated", serde_json::to_value(&*zone)?);
|
||||
@@ -602,8 +635,7 @@ fn expire_local_thermostat_overrides(state: &AppState, zones: &mut [Zone], sched
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
let expired = zone.local_thermostat_power == Some(false)
|
||||
&& zone.local_thermostat_resume_at.as_ref().map(|at| at <= &now).unwrap_or(false);
|
||||
let expired = zone.local_thermostat_resume_at.as_ref().map(|at| at <= &now).unwrap_or(false);
|
||||
if !expired { continue; }
|
||||
reset_local_thermostat_override(zone);
|
||||
refresh_zone_runtime_target(zone, schedules, house_mode);
|
||||
@@ -620,7 +652,7 @@ fn expire_local_thermostat_overrides(state: &AppState, zones: &mut [Zone], sched
|
||||
fn next_local_thermostat_resume_delay(state: &AppState) -> Result<Option<Duration>, AppError> {
|
||||
let now = Utc::now();
|
||||
Ok(state.db.list_zones()?.into_iter()
|
||||
.filter(|zone| zone.local_thermostat_power == Some(false))
|
||||
.filter(local_thermostat_handback_is_active)
|
||||
.filter_map(|zone| zone.local_thermostat_resume_at)
|
||||
.map(|at| (at - now.clone()).to_std().unwrap_or(Duration::ZERO))
|
||||
.min())
|
||||
@@ -660,7 +692,9 @@ fn manual_override_matches_baseline(zone: &Zone, device: &Device) -> bool {
|
||||
|
||||
fn persist_manual_override_clear(state: &AppState, zone: &mut Zone, source: &str, restored: bool) -> Result<bool, AppError> {
|
||||
if !reset_device_manual_override(zone) { return Ok(false); }
|
||||
zone.updated_at = Utc::now();
|
||||
let now = Utc::now();
|
||||
let local_resume_rearmed = restored && rearm_local_thermostat_resume(zone, now.clone());
|
||||
zone.updated_at = now;
|
||||
state.db.save_zone(zone)?;
|
||||
state.broadcast("zone.updated", serde_json::to_value(&*zone)?);
|
||||
let (kind, message) = if restored {
|
||||
@@ -669,7 +703,9 @@ fn persist_manual_override_clear(state: &AppState, zone: &mut Zone, source: &str
|
||||
("zone.device_manual_override_cleared", format!("Manual device control ended for {}", zone.name))
|
||||
};
|
||||
state.log("info", kind, &message, json!({
|
||||
"zone_id": zone.id, "device_id": zone.device_id, "source": source
|
||||
"zone_id": zone.id, "device_id": zone.device_id, "source": source,
|
||||
"local_thermostat_resume_rearmed": local_resume_rearmed,
|
||||
"local_thermostat_resume_at": zone.local_thermostat_resume_at,
|
||||
}));
|
||||
Ok(true)
|
||||
}
|
||||
@@ -2407,6 +2443,39 @@ mod tests {
|
||||
assert_eq!(LOCAL_THERMOSTAT_RESUME_DELAY_MINUTES, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_thermostat_off_restarts_backend_handback_deadline() {
|
||||
let mut zone = test_zone("device");
|
||||
let first = Utc::now();
|
||||
set_local_thermostat_power(&mut zone, false, first.clone());
|
||||
let first_deadline = zone.local_thermostat_resume_at.clone().unwrap();
|
||||
assert_eq!(first_deadline, first.clone() + chrono::Duration::minutes(15));
|
||||
|
||||
let second = first + chrono::Duration::minutes(4);
|
||||
set_local_thermostat_power(&mut zone, true, second.clone());
|
||||
assert!(zone.local_thermostat_resume_at.is_none());
|
||||
set_local_thermostat_power(&mut zone, false, second.clone());
|
||||
assert_eq!(zone.local_thermostat_resume_at, Some(second + chrono::Duration::minutes(15)));
|
||||
assert!(zone.local_thermostat_resume_at.clone().unwrap() > first_deadline);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_device_takeover_suspends_local_handback_until_control_returns() {
|
||||
let mut zone = test_zone("device");
|
||||
let now = Utc::now();
|
||||
set_local_thermostat_power(&mut zone, false, now.clone());
|
||||
assert!(local_thermostat_handback_is_active(&zone));
|
||||
|
||||
zone.device_manual_override = true;
|
||||
assert!(!local_thermostat_handback_is_active(&zone));
|
||||
|
||||
zone.device_manual_override = false;
|
||||
let returned = now + chrono::Duration::minutes(7);
|
||||
assert!(rearm_local_thermostat_resume(&mut zone, returned.clone()));
|
||||
assert_eq!(zone.local_thermostat_resume_at, Some(returned + chrono::Duration::minutes(15)));
|
||||
assert!(local_thermostat_handback_is_active(&zone));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded_gree_setpoint_does_not_create_manual_override() {
|
||||
let zone = test_zone("device");
|
||||
|
||||
Reference in New Issue
Block a user