v0.16.3
This commit is contained in:
1 parent
f376f0a7b6
commit
8059e16d12
36 files changed
+1140
-193
No files matched your search
+79
-19
@@ -541,6 +541,17 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
}
|
||||
let schedules = state.db.list_schedules()?;
|
||||
let mut zone_snapshot = state.db.list_zones()?;
|
||||
// Only an enabled, compiled Flow action is allowed to retain simple heating
|
||||
// ownership. Disabling, deleting or recompiling a Flow immediately removes
|
||||
// stale heating profiles in the next control cycle.
|
||||
let enabled_heat_actions: HashSet<(String, String, String)> = state.db.list_automations()?
|
||||
.into_iter()
|
||||
.filter(|item| item.enabled)
|
||||
.filter_map(|item| {
|
||||
let profile = item.action_zone_simple_heat?;
|
||||
Some((profile.flow_id, profile.flow_node_id, item.action_zone_id?))
|
||||
})
|
||||
.collect();
|
||||
// Local/temporary thermostat ownership has its own deadlines. Expire and activate sessions independently.
|
||||
expire_local_thermostat_overrides(state, &mut zone_snapshot, &schedules, &settings.house_mode)
|
||||
.await?;
|
||||
@@ -576,6 +587,16 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
continue;
|
||||
};
|
||||
let cycle_started_at = zone.updated_at.clone();
|
||||
if zone.simple_heat.as_ref().is_some_and(|profile| {
|
||||
!enabled_heat_actions.contains(&(profile.flow_id.clone(), profile.flow_node_id.clone(), zone.id.clone()))
|
||||
}) {
|
||||
zone.simple_heat = None;
|
||||
zone.simple_heat_runtime = SimpleHeatRuntime::default();
|
||||
if zone.control_source == "automation.flow" {
|
||||
zone.control_source = "automation".into();
|
||||
zone.control_reason = "Simple heating Flow disabled; thermostat idle".into();
|
||||
}
|
||||
}
|
||||
let outdoor_temperature = zone_outdoor_sensor_results
|
||||
.remove(&zone.id)
|
||||
.flatten()
|
||||
@@ -784,7 +805,15 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (preset, target) = resolve_zone_target(&zone, active_schedule, effective_mode);
|
||||
let simple_profile = zone.simple_heat.clone().filter(|_| {
|
||||
zone.control_owner == "automation" && zone.control_source == "automation.flow"
|
||||
&& zone.manual_preset.is_none() && zone.manual_setpoint.is_none()
|
||||
});
|
||||
let (preset, target) = if let Some(profile) = simple_profile.as_ref() {
|
||||
("auto".into(), if night_active { profile.night_target } else { profile.day_target })
|
||||
} else {
|
||||
resolve_zone_target(&zone, active_schedule, effective_mode)
|
||||
};
|
||||
zone.active_preset = preset;
|
||||
zone.effective_setpoint = Some(target);
|
||||
|
||||
@@ -802,17 +831,53 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
continue;
|
||||
};
|
||||
|
||||
let simple_decision = simple_profile.as_ref().map(|profile| {
|
||||
simple_heat_step(profile, &mut zone.simple_heat_runtime, temp, night_active, Utc::now())
|
||||
});
|
||||
if simple_decision.is_some_and(|decision| !decision.power) {
|
||||
zone.heat_coasting = false;
|
||||
zone.demand = false;
|
||||
zone.demand_since = None;
|
||||
zone.effective_mode = "off".into();
|
||||
zone.device_setpoint = if device.power { Some(device.target_temperature) } else { None };
|
||||
clear_compressor_pending(&mut zone, true);
|
||||
if device.power && device.online && device.communication_failures == 0 {
|
||||
let protection = chrono::Duration::seconds(zone.min_on_seconds.max(settings.compressor_protection_seconds).max(30) as i64);
|
||||
let safe_to_switch_off = !settings.compressor_protection_enabled
|
||||
|| zone.last_power_change_at.is_none_or(|at| Utc::now().signed_duration_since(at) >= protection);
|
||||
if safe_to_switch_off {
|
||||
match send_zone_command_if_owned(state, &zone.id, &zone.device_id, DeviceCommand {
|
||||
power: Some(false), mode: Some("heat".into()), ..Default::default()
|
||||
}).await {
|
||||
Ok(Some(updated_device)) => {
|
||||
if !updated_device.power {
|
||||
zone.last_power_change_at = Some(Utc::now());
|
||||
zone.last_action_at = Some(Utc::now());
|
||||
zone.device_setpoint = None;
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => state.log("warn", "zone.simple_heat_day_off_error", &err.to_string(),
|
||||
json!({"zone_id": zone.id})),
|
||||
}
|
||||
}
|
||||
}
|
||||
record_zone_history(state, &zone, outdoor_temperature, settings.poll_interval_seconds);
|
||||
publish_persisted_zone_cycle(state, persist_zone_cycle(state, &zone, cycle_started_at)?)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let half = zone.hysteresis_for_mode(effective_mode) / 2.0;
|
||||
let previous_demand = zone.demand;
|
||||
let previous_heat_coasting = zone.heat_coasting;
|
||||
zone.heat_coasting = heat_coasting_next_state(
|
||||
zone.heat_coasting,
|
||||
effective_mode,
|
||||
temp,
|
||||
target,
|
||||
zone.heat_anticipation_c,
|
||||
);
|
||||
zone.demand = match effective_mode {
|
||||
zone.heat_coasting = if let Some(decision) = simple_decision {
|
||||
decision.coasting
|
||||
} else {
|
||||
heat_coasting_next_state(zone.heat_coasting, effective_mode, temp, target, zone.heat_anticipation_c)
|
||||
};
|
||||
zone.demand = if let Some(decision) = simple_decision {
|
||||
decision.demand
|
||||
} else { match effective_mode {
|
||||
"heat" => {
|
||||
if temp <= target - half {
|
||||
true
|
||||
@@ -831,7 +896,7 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
zone.demand
|
||||
}
|
||||
}
|
||||
};
|
||||
}};
|
||||
if zone.demand && !previous_demand {
|
||||
zone.demand_since = Some(Utc::now());
|
||||
zone.target_alerted_at = None;
|
||||
@@ -874,15 +939,10 @@ async fn control_zones(state: &AppState) -> Result<()> {
|
||||
"heat" => target - zone.standby_offset_c.max(0.5),
|
||||
_ => target + zone.standby_offset_c.max(0.5),
|
||||
};
|
||||
let desired_device_target = round_device_setpoint(
|
||||
effective_mode,
|
||||
delivering_heat,
|
||||
if delivering_heat {
|
||||
active_target
|
||||
} else {
|
||||
standby_target
|
||||
},
|
||||
);
|
||||
let desired_device_target = simple_decision.map(|decision| decision.device_target).unwrap_or_else(|| {
|
||||
round_device_setpoint(effective_mode, delivering_heat,
|
||||
if delivering_heat { active_target } else { standby_target })
|
||||
});
|
||||
// Report only the last confirmed device state here. The desired target belongs to
|
||||
// effective_setpoint/command planning until a device command succeeds.
|
||||
zone.device_setpoint = if device.power {
|
||||
|
||||
Reference in new issue
Block a user