This commit is contained in:
Mateusz Gruszczyński
2026-08-28 13:09:11 +02:00
parent 7b5c9e4528
commit 9a5e4892f4
10 changed files with 38 additions and 68 deletions
-1
View File
@@ -874,7 +874,6 @@ async fn apply_zone_control_patch(state: &AppState, id: &str, patch: ZoneControl
started_at,
activated_at: if immediate_activation { Some(now.clone()) } else { activated_at.clone() },
state: state_value.into(),
generation: existing_session.as_ref().map(|session| session.generation.saturating_add(1)).unwrap_or(1),
active_mode,
restore_zone_enabled,
restore_local_thermostat_power: if editing_active {
+7 -32
View File
@@ -651,8 +651,7 @@ fn local_thermostat_handback_is_active(zone: &Zone) -> bool {
/// deliberately untouched; the two ownership mechanisms have independent cleanup paths.
pub fn reset_local_thermostat_override(zone: &mut Zone) -> bool {
let temporary_active = zone.temporary_quick_thermostat.as_ref()
.map(|session| session.activated_at.is_some()
|| (session.generation == 0 && session.start_kind == "now" && zone.local_thermostat_power == Some(true)))
.map(|session| session.activated_at.is_some())
.unwrap_or(false);
if temporary_active {
let Some(session) = zone.temporary_quick_thermostat.as_mut() else { return false; };
@@ -691,14 +690,10 @@ pub fn reset_local_thermostat_override(zone: &mut Zone) -> bool {
}
pub fn temporary_quick_thermostat_is_active(zone: &Zone, now: DateTime<Utc>) -> bool {
let Some(session) = zone.temporary_quick_thermostat.as_ref() else { return false; };
if session.activated_at.as_ref().map(|at| at <= &now).unwrap_or(false) { return true; }
// Legacy recovery only for old immediate sessions persisted before activated_at existed.
// Fresh delay/at sessions must never inherit activity from an unrelated local Quick ON.
session.generation == 0
&& session.start_kind == "now"
&& session.started_at <= now
&& zone.local_thermostat_power == Some(true)
zone.temporary_quick_thermostat.as_ref()
.and_then(|session| session.activated_at.as_ref())
.map(|activated_at| activated_at <= &now)
.unwrap_or(false)
}
fn temporary_quick_thermostat_hard_deadline(session: &TemporaryQuickThermostat) -> Option<DateTime<Utc>> {
@@ -3273,7 +3268,6 @@ mod tests {
started_at: now.clone(),
activated_at: Some(now),
state: "active".into(),
generation: 1,
active_mode: Some("heat".into()),
restore_zone_enabled: Some(true),
restore_local_thermostat_power: None,
@@ -3371,17 +3365,15 @@ mod tests {
}
#[test]
fn legacy_immediate_session_can_use_local_power_fallback_but_fresh_session_cannot() {
fn temporary_session_without_activation_marker_is_never_active() {
let now = Utc::now();
let mut zone = test_zone("device");
zone.local_thermostat_power = Some(true);
let mut session = temporary_session(now);
session.activated_at = None;
session.generation = 0;
session.state = "scheduled".into();
zone.temporary_quick_thermostat = Some(session);
assert!(temporary_quick_thermostat_is_active(&zone, now));
zone.temporary_quick_thermostat.as_mut().unwrap().generation = 1;
assert!(!temporary_quick_thermostat_is_active(&zone, now));
}
@@ -3454,23 +3446,6 @@ mod tests {
assert!(zone.temporary_quick_thermostat.as_ref().unwrap().condition_started_at.is_none());
}
#[test]
fn legacy_active_temporary_session_without_activated_at_still_evaluates_condition() {
let now = Utc::now();
let mut zone = test_zone("device");
zone.current_temperature = Some(23.0);
zone.local_thermostat_power = Some(true);
let mut session = temporary_session(now.clone());
session.activated_at = None;
session.condition_started_at = Some(now.clone() - chrono::Duration::hours(1));
zone.temporary_quick_thermostat = Some(session);
assert_eq!(
evaluate_temporary_quick_thermostat_condition(&mut zone, now, Some(now), 10),
Some("temperature_stable".into())
);
}
#[test]
fn temporary_setpoint_keeps_priority_over_active_schedule() {
let mut zone = test_zone("device");
-4
View File
@@ -288,10 +288,6 @@ pub struct TemporaryQuickThermostat {
/// Explicit lifecycle state: scheduled | waiting_master | paused_manual | active.
#[serde(default = "default_temporary_state")]
pub state: String,
/// Fresh sessions use generation >= 1. Generation 0 identifies legacy payloads that
/// predate explicit activated_at/state semantics and may use the compatibility fallback.
#[serde(default)]
pub generation: u64,
/// Heat/cool mode captured when ownership really starts. It keeps a temporary session
/// independent from later whole-house mode changes until hand-back.
#[serde(default)]