v0.15.21
This commit is contained in:
1 parent
aa1cb05b2f
commit
8a7501e4ab
9 files changed
+175
-32
No files matched your search
@@ -12,6 +12,64 @@ fn device_has_thermostat_zone(device_id: &str, zones: &[Zone]) -> bool {
|
||||
zones.iter().any(|zone| zone.device_id == device_id)
|
||||
}
|
||||
|
||||
fn flow_zone_action_already_satisfied(
|
||||
zone: &Zone,
|
||||
device_power: bool,
|
||||
device_swing_vertical: u8,
|
||||
device_swing_horizontal: u8,
|
||||
preset: Option<&str>,
|
||||
action: &DeviceCommand,
|
||||
) -> bool {
|
||||
let mode_satisfied = match action.mode.as_deref() {
|
||||
Some("auto") => zone.inherit_house_mode,
|
||||
Some(mode) if matches!(mode, "heat" | "cool") => {
|
||||
!zone.inherit_house_mode && zone.mode == mode
|
||||
}
|
||||
Some(_) => false,
|
||||
None => true,
|
||||
};
|
||||
if !mode_satisfied {
|
||||
return false;
|
||||
}
|
||||
|
||||
let preset_satisfied = match preset.unwrap_or("auto") {
|
||||
"auto" => zone.manual_preset.is_none()
|
||||
&& zone.manual_setpoint.is_none()
|
||||
&& zone.manual_override_until.is_none(),
|
||||
"custom" => action.target_temperature.is_some_and(|target| {
|
||||
zone.manual_preset.as_deref() == Some("custom")
|
||||
&& zone.manual_setpoint.is_some_and(|value| {
|
||||
(value - normalize_thermostat_target(target.clamp(8.0, 30.0))).abs() < 0.05
|
||||
})
|
||||
}),
|
||||
preset @ ("comfort" | "sleep" | "away") => {
|
||||
zone.manual_preset.as_deref() == Some(preset) && zone.manual_setpoint.is_none()
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !preset_satisfied {
|
||||
return false;
|
||||
}
|
||||
|
||||
let power_satisfied = match action.power {
|
||||
Some(false) => !device_power
|
||||
&& !zone.demand
|
||||
&& zone.effective_mode == "off"
|
||||
&& zone.device_setpoint.is_none(),
|
||||
// A Flow thermostat ON action establishes durable thermostat intent. Once the same
|
||||
// mode/preset/target is already present, the normal zone controller owns retries,
|
||||
// compressor lockout and physical power-on. Re-firing the Flow would only refresh
|
||||
// metadata and produce duplicate automation.fired events.
|
||||
Some(true) | None => true,
|
||||
};
|
||||
if !power_satisfied {
|
||||
return false;
|
||||
}
|
||||
|
||||
action.swing_vertical.map(|value| value == device_swing_vertical).unwrap_or(true)
|
||||
&& action.swing_horizontal.map(|value| value == device_swing_horizontal).unwrap_or(true)
|
||||
}
|
||||
|
||||
async fn run_automations(state: &AppState) -> Result<()> {
|
||||
if state.settings.read().await.emergency_stop_enabled {
|
||||
return Ok(());
|
||||
@@ -109,6 +167,32 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
let zones = state.db.list_zones()?;
|
||||
let groups = state.db.list_groups()?;
|
||||
|
||||
// Level-style Flow conditions (stable_for + rate_limit) intentionally remain true
|
||||
// after they first fire. Do not execute or log the same thermostat command forever.
|
||||
// This check happens before same-cycle target arbitration so two equivalent OFF
|
||||
// branches do not manufacture automation.conflict noise either.
|
||||
if let Some(zone_id) = item.action_zone_id.as_deref() {
|
||||
if let Some(zone) = zones.iter().find(|zone| zone.id == zone_id) {
|
||||
if let Some(device) = state.db.get_device(&zone.device_id)? {
|
||||
if flow_zone_action_already_satisfied(
|
||||
zone,
|
||||
device.power,
|
||||
device.swing_vertical,
|
||||
device.swing_horizontal,
|
||||
item.action_zone_preset.as_deref(),
|
||||
&effective_action,
|
||||
) {
|
||||
let before_runtime = item.flow_runtime.clone();
|
||||
flow_acknowledge_change_gates(&mut item);
|
||||
if item.flow_runtime != before_runtime {
|
||||
state.db.save_automation(&item)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if item.action_group_id.is_none() && item.action_zone_id.is_none()
|
||||
&& device_blocked_by_disabled_zone(&item.action_device_id, &zones)
|
||||
&& effective_action.power != Some(true)
|
||||
|
||||
Reference in new issue
Block a user