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
+54
View File
@@ -385,6 +385,60 @@ async fn control_zones(state: &AppState) -> Result<()> {
}
let active_schedule = active_schedule_for_zone(&zone, &schedules, Local::now());
// Heat/Cool configuration alone is not an instruction to run forever on the implicit
// Comfort profile. After local/global/group OFF is handed back to automation, a zone
// with no active schedule, quick/manual target, temporary session or active scoped
// controller stays physically OFF. This also makes gaps between schedule windows true
// OFF periods instead of silently falling back to Comfort and recreating demand.
if !zone_has_active_thermostat_intent(&zone, active_schedule, Utc::now()) {
clear_compressor_pending(&mut zone, true);
zone.effective_mode = "off".into();
zone.effective_setpoint = None;
zone.device_setpoint = None;
zone.demand = false;
zone.demand_since = None;
zone.target_alerted_at = None;
if zone.control_owner == "automation" {
zone.control_reason = "Automation idle: no active schedule or explicit thermostat request".into();
zone.control_resume_at = None;
}
if device.online && device.communication_failures == 0 && device.power {
match send_zone_command_if_owned(
state,
&zone.id,
&zone.device_id,
DeviceCommand { power: Some(false), ..Default::default() },
).await {
Ok(Some(updated_device)) => {
let transition_at = Utc::now();
if device.power != updated_device.power {
zone.last_power_change_at = Some(transition_at);
}
zone.last_action_at = Some(transition_at);
state.log("info", "zone.automation_idle_off", &format!("Zone {} remains OFF: no active thermostat intent", zone.name), json!({
"zone_id": zone.id,
"device_id": zone.device_id,
"active_schedule": false,
"manual_preset": zone.manual_preset,
"manual_setpoint": zone.manual_setpoint,
"control_source": zone.control_source,
}));
}
Ok(None) => {}
Err(err) => state.log("error", "zone.automation_idle_off_error", &err.to_string(), json!({
"zone_id": zone.id, "device_id": zone.device_id
})),
}
}
record_zone_history(state, &zone, outdoor_temperature, settings.poll_interval_seconds);
let persisted_zone = persist_zone_cycle(state, &zone, cycle_started_at)?;
state.broadcast("zone.updated", serde_json::to_value(&persisted_zone)?);
continue;
}
let (preset, target) = resolve_zone_target(&zone, active_schedule, effective_mode);
zone.active_preset = preset;
zone.effective_setpoint = Some(target);