v0.9.4-fix

This commit is contained in:
Mateusz Gruszczyński
2026-09-01 14:59:00 +02:00
parent 3c490b3064
commit 150cf6cb7f
13 changed files with 285 additions and 62 deletions
+34 -17
View File
@@ -20,25 +20,36 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
let configured_effective_mode_owned = effective_zone_mode(&zone, &settings.house_mode);
refresh_control_ownership(&mut zone, true);
let configured_effective_mode = configured_effective_mode_owned.as_str();
let target_mode = if configured_effective_mode == "off" { zone.mode.as_str() } else { configured_effective_mode };
let active_for_target = active_schedule_for_zone(&zone, &schedules, now);
let has_intent_source = zone_has_thermostat_intent_source(&zone, active_for_target, Utc::now());
let automation_idle = zone.enabled
&& !zone.device_manual_override
&& zone.local_thermostat_power != Some(false)
&& configured_effective_mode != "off"
&& !has_intent_source;
let automatic_active = zone.enabled
&& !zone.device_manual_override
&& zone.local_thermostat_power != Some(false)
&& configured_effective_mode != "off"
&& has_intent_source;
let manual_device_mode = device.map(|item| if item.power { item.mode.as_str() } else { "off" });
let effective_mode = if zone.device_manual_override {
manual_device_mode.unwrap_or(configured_effective_mode)
} else if zone.local_thermostat_power == Some(false) {
"off"
} else {
} else if automatic_active {
configured_effective_mode
} else {
"off"
};
// Keep the thermostat target readable even while the zone/group/house control is off.
// Home Assistant climate entities otherwise expose target_temperature as unknown.
let target_mode = if configured_effective_mode == "off" { zone.mode.as_str() } else { configured_effective_mode };
let active_for_target = active_schedule_for_zone(&zone, &schedules, now);
let (resolved_preset, resolved_target) = resolve_zone_target(&zone, active_for_target, target_mode);
let active = if effective_mode == "off" { None } else { active_for_target };
let next_events = if effective_mode == "off" {
let active = if automatic_active { active_for_target } else { None };
// Future schedule events remain visible while the zone is currently idle between
// windows; idle must not be confused with a disabled schedule system.
let next_events = if configured_effective_mode == "off" {
Vec::new()
} else {
next_schedule_events(&zone, &schedules, effective_mode, now, 8)
next_schedule_events(&zone, &schedules, configured_effective_mode, now, 8)
};
for event in next_events.iter().take(2) {
let mut event = event.clone();
@@ -57,23 +68,25 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
mode: effective_mode.to_string(),
configured_mode: zone.mode.clone(),
inherit_house_mode: zone.inherit_house_mode,
preset: resolved_preset,
preset: if has_intent_source { resolved_preset } else { "auto".into() },
preset_override: zone.manual_preset.clone(),
current_temperature: zone.current_temperature,
target_temperature: if zone.device_manual_override || !zone.enabled || effective_mode == "off" {
target_temperature: if !has_intent_source {
None
} else if zone.device_manual_override || !zone.enabled || effective_mode == "off" {
// A remote/manual takeover may leave a physical standby target persisted in the
// zone runtime snapshot. Never publish that value as the thermostat target.
// zone runtime snapshot. Publish only the underlying explicit thermostat target.
Some(resolved_target)
} else {
zone.effective_setpoint.or(Some(resolved_target))
},
device_setpoint: device.filter(|item| item.power).map(|item| item.target_temperature),
desired_power: zone.enabled && effective_mode != "off" && !zone.device_manual_override,
desired_power: automatic_active,
desired_mode: effective_mode.to_string(),
actual_power: device.map(|item| item.power),
actual_mode: device.map(|item| if item.power { item.mode.clone() } else { "off".into() }),
actual_setpoint: device.filter(|item| item.power).map(|item| item.target_temperature),
demand: zone.enabled && effective_mode != "off" && !zone.device_manual_override && zone.demand,
demand: automatic_active && zone.demand,
control_source: zone.control_temperature_source.clone(),
manual_override_until: zone.manual_override_until,
local_thermostat_power: zone.local_thermostat_power,
@@ -84,8 +97,12 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
control_command_source: zone.control_source.clone(),
control_since: zone.control_since,
resume_at: zone.control_resume_at,
control_reason: zone.control_reason.clone(),
blocked_reason: if zone.device_manual_override { Some("manual_override".into()) } else if zone.lockout_until.map(|until| until > Utc::now()).unwrap_or(false) { Some(zone.lockout_reason.clone().unwrap_or_else(|| "lockout".into())) } else if !zone.enabled { Some("zone_disabled".into()) } else if device.map(|d| !d.online || d.communication_failures > 0).unwrap_or(true) { Some("offline".into()) } else { None },
control_reason: if automation_idle {
"Automation idle: no active schedule or explicit thermostat request".into()
} else {
zone.control_reason.clone()
},
blocked_reason: if zone.device_manual_override { Some("manual_override".into()) } else if zone.lockout_until.map(|until| until > Utc::now()).unwrap_or(false) { Some(zone.lockout_reason.clone().unwrap_or_else(|| "lockout".into())) } else if !zone.enabled { Some("zone_disabled".into()) } else if device.map(|d| !d.online || d.communication_failures > 0).unwrap_or(true) { Some("offline".into()) } else if automation_idle { Some("no_active_thermostat_intent".into()) } else { None },
lockout_until: zone.lockout_until,
current_schedule_id: active.map(|item| item.id.clone()),
current_schedule_name: active.map(|item| item.name.clone()),