v0.16.4
This commit is contained in:
1 parent
05185a94f7
commit
bb5caa769f
28 files changed
+592
-130
No files matched your search
+105
-20
@@ -533,6 +533,102 @@ async fn handle_zone_pre_control_state(
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Flow actions are durable zone intent, not just one-shot physical commands. An
|
||||
/// inactive/deleted Flow must release any remaining thermostat demand and compressor
|
||||
/// task even if a previous version persisted an enabled compiled automation.
|
||||
fn release_inactive_flow_zone_intent(
|
||||
zone: &mut Zone,
|
||||
active_actions: &HashSet<(String, String, String)>,
|
||||
) -> bool {
|
||||
let old_profile = zone.simple_heat.as_ref();
|
||||
let stale_simple_heat = old_profile.is_some_and(|profile| {
|
||||
!active_actions.contains(&(
|
||||
profile.flow_id.clone(), profile.flow_node_id.clone(), zone.id.clone(),
|
||||
))
|
||||
});
|
||||
let actual_owner = zone.flow_control_id.as_deref()
|
||||
.zip(zone.flow_control_node_id.as_deref())
|
||||
.or_else(|| old_profile.map(|profile| (profile.flow_id.as_str(), profile.flow_node_id.as_str())));
|
||||
let active_owner = match actual_owner {
|
||||
Some((flow_id, node_id)) => active_actions.contains(&(
|
||||
flow_id.to_string(), node_id.to_string(), zone.id.clone(),
|
||||
)),
|
||||
None => active_actions.iter().any(|(_, _, zone_id)| zone_id == &zone.id),
|
||||
};
|
||||
// Never clear genuine manual/local/temporary control because a disabled Flow
|
||||
// happened to be the previous owner of the same thermostat.
|
||||
let flow_owned = zone.control_source == "automation.flow"
|
||||
|| (zone.control_source == "automation.device" && zone.flow_control_id.is_some());
|
||||
let stale_control = flow_owned
|
||||
&& !zone.device_manual_override && zone.local_thermostat_power.is_none()
|
||||
&& zone.temporary_quick_thermostat.is_none()
|
||||
&& !active_owner;
|
||||
if stale_simple_heat {
|
||||
zone.simple_heat = None;
|
||||
zone.simple_heat_runtime = SimpleHeatRuntime::default();
|
||||
}
|
||||
if stale_control {
|
||||
zone.manual_preset = None;
|
||||
zone.manual_setpoint = None;
|
||||
zone.manual_override_until = None;
|
||||
zone.flow_control_id = None;
|
||||
zone.flow_control_node_id = None;
|
||||
zone.control_source = "automation".into();
|
||||
zone.control_reason = "Disabled/deleted Flow released thermostat control".into();
|
||||
zone.control_since = Some(Utc::now());
|
||||
zone.demand = false;
|
||||
zone.demand_since = None;
|
||||
zone.heat_coasting = false;
|
||||
clear_compressor_pending(zone, true);
|
||||
}
|
||||
stale_control || stale_simple_heat
|
||||
}
|
||||
|
||||
fn active_flow_zone_action_ids(state: &AppState) -> Result<HashSet<(String, String, String)>> {
|
||||
let zone_by_device: HashMap<String, String> = state.db.list_zones()?
|
||||
.into_iter().map(|zone| (zone.device_id, zone.id)).collect();
|
||||
let mut owners = HashSet::new();
|
||||
for rule in state.db.list_automations()?.into_iter().filter(|item| item.enabled) {
|
||||
let (Some(flow_id), Some(node_id)) = (rule.flow_id, rule.flow_node_id) else { continue; };
|
||||
// Both explicit Flow thermostat actions and Flow commands addressed to the
|
||||
// underlying GREE device can own durable thermostat state.
|
||||
let zone_id = rule.action_zone_id.or_else(|| {
|
||||
if rule.action_group_id.is_some() || rule.action_ha_domain.is_some() {
|
||||
None
|
||||
} else {
|
||||
zone_by_device.get(&rule.action_device_id).cloned()
|
||||
}
|
||||
});
|
||||
if let Some(zone_id) = zone_id {
|
||||
owners.insert((flow_id, node_id, zone_id));
|
||||
}
|
||||
}
|
||||
Ok(owners)
|
||||
}
|
||||
|
||||
/// Call only with the automation, schedule and thermostat-cycle locks held, in
|
||||
/// that order (as in the Flow update/delete handlers).
|
||||
pub(crate) async fn release_inactive_flow_intents(state: &AppState) -> Result<()> {
|
||||
let active_actions = active_flow_zone_action_ids(state)?;
|
||||
for item in state.db.list_zones()? {
|
||||
if item.control_source != "automation.flow"
|
||||
&& !(item.control_source == "automation.device" && item.flow_control_id.is_some())
|
||||
&& item.simple_heat.is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let _zone_guard = state.lock_zone_operation(&item.id).await;
|
||||
let Some(mut zone) = state.db.get_zone(&item.id)? else { continue; };
|
||||
if !release_inactive_flow_zone_intent(&mut zone, &active_actions) { continue; }
|
||||
zone.revision = zone.revision.saturating_add(1);
|
||||
zone.updated_at = Utc::now();
|
||||
state.db.save_zone(&zone)?;
|
||||
state.broadcast("zone.updated", serde_json::to_value(&zone)?);
|
||||
state.log("info", "flow.zone_released", "Inactive Flow released thermostat intent", json!({"zone_id": zone.id}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn control_zones(state: &AppState) -> Result<()> {
|
||||
let _cycle_guard = state.lock_zone_control_cycle().await;
|
||||
let settings = state.settings.read().await.clone();
|
||||
@@ -541,17 +637,8 @@ 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();
|
||||
// Flow ownership is valid only while its exact compiled action is enabled.
|
||||
let active_flow_actions = active_flow_zone_action_ids(state)?;
|
||||
// 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?;
|
||||
@@ -587,15 +674,13 @@ 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();
|
||||
}
|
||||
if release_inactive_flow_zone_intent(&mut zone, &active_flow_actions) {
|
||||
// Persist even if this device is currently unavailable and the control
|
||||
// cycle would otherwise return early without saving the zone.
|
||||
zone.revision = zone.revision.saturating_add(1);
|
||||
zone.updated_at = Utc::now();
|
||||
state.db.save_zone(&zone)?;
|
||||
state.broadcast("zone.updated", serde_json::to_value(&zone)?);
|
||||
}
|
||||
let outdoor_temperature = zone_outdoor_sensor_results
|
||||
.remove(&zone.id)
|
||||
|
||||
Reference in new issue
Block a user