v0.16.4
This commit is contained in:
1 parent
05185a94f7
commit
bb5caa769f
28 files changed
+592
-130
No files matched your search
@@ -26,6 +26,8 @@ fn flow_off_physical_mode(action: &DeviceCommand) -> Option<String> {
|
||||
|
||||
fn flow_zone_action_already_satisfied(
|
||||
zone: &Zone,
|
||||
flow_id: Option<&str>,
|
||||
flow_node_id: Option<&str>,
|
||||
device_power: bool,
|
||||
device_swing_vertical: u8,
|
||||
device_swing_horizontal: u8,
|
||||
@@ -35,9 +37,18 @@ fn flow_zone_action_already_satisfied(
|
||||
standby_offset_c: Option<f64>,
|
||||
simple_heat: Option<&SimpleHeatSettings>,
|
||||
) -> bool {
|
||||
// Matching settings from another (or formerly deleted) Flow are not ownership of
|
||||
// this action. It needs to execute and record its own provenance.
|
||||
if zone.control_source != "automation.flow"
|
||||
// Legacy zone rows without attribution must run once to claim their
|
||||
// current Flow; matching settings alone do not prove ownership.
|
||||
|| (flow_id.is_some() && zone.flow_control_id.as_deref() != flow_id)
|
||||
|| (flow_node_id.is_some() && zone.flow_control_node_id.as_deref() != flow_node_id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(profile) = simple_heat {
|
||||
return zone.simple_heat.as_ref() == Some(profile)
|
||||
&& zone.control_source == "automation.flow";
|
||||
return zone.simple_heat.as_ref() == Some(profile);
|
||||
}
|
||||
if heat_anticipation_c.is_some_and(|value| (zone.heat_anticipation_c - value).abs() > 0.001)
|
||||
|| standby_offset_c.is_some_and(|value| (zone.standby_offset_c - value).abs() > 0.001)
|
||||
@@ -115,7 +126,13 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
"flow" => {
|
||||
let before_runtime = item.flow_runtime.clone();
|
||||
let result = flow_conditions_match(state, &devices, &mut item).await;
|
||||
if item.flow_runtime != before_runtime { state.db.save_automation(&item)?; }
|
||||
if item.flow_runtime != before_runtime
|
||||
&& !state.db.save_flow_runtime_if_current(&item)?
|
||||
{
|
||||
// A Flow was disabled, deleted or recompiled during asynchronous
|
||||
// condition evaluation. Never execute or resurrect its old snapshot.
|
||||
continue;
|
||||
}
|
||||
match result {
|
||||
Ok((value, trace)) => {
|
||||
trigger_trace = trace;
|
||||
@@ -146,7 +163,11 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
let Some(latest_item) = state.db.get_automation(&item.id)? else { continue; };
|
||||
if latest_item.updated_at != item.updated_at { continue; }
|
||||
if !latest_item.enabled || latest_item.updated_at != item.updated_at
|
||||
|| !state.db.flow_output_enabled(latest_item.flow_id.as_deref())?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
item = latest_item;
|
||||
let mut effective_action = item.action.clone();
|
||||
let mut effective_ha_data = item.action_ha_data.clone();
|
||||
@@ -200,6 +221,8 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
if let Some(device) = state.db.get_device(&zone.device_id)? {
|
||||
if flow_zone_action_already_satisfied(
|
||||
zone,
|
||||
item.flow_id.as_deref(),
|
||||
item.flow_node_id.as_deref(),
|
||||
device.power,
|
||||
device.swing_vertical,
|
||||
device.swing_horizontal,
|
||||
@@ -212,7 +235,9 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
let before_runtime = item.flow_runtime.clone();
|
||||
flow_acknowledge_change_gates(&mut item);
|
||||
if item.flow_runtime != before_runtime {
|
||||
state.db.save_automation(&item)?;
|
||||
// The action lock prevents Flow edits here. A runtime-only
|
||||
// update still guarantees we cannot overwrite enablement.
|
||||
if !state.db.save_flow_runtime_if_current(&item)? { continue; }
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -296,6 +321,7 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
state, zone_id, item.action_zone_preset.as_deref(), &effective_action,
|
||||
item.action_zone_heat_anticipation_c, item.action_zone_standby_offset_c,
|
||||
item.action_zone_simple_heat.as_ref(),
|
||||
item.flow_id.as_deref(), item.flow_node_id.as_deref(),
|
||||
).await
|
||||
} else if let Some(group_id) = item.action_group_id.as_deref() {
|
||||
let group_mode = effective_action.mode.as_deref().map(|mode| if mode == "auto" { "house".to_string() } else { mode.to_string() });
|
||||
@@ -306,7 +332,10 @@ async fn run_automations(state: &AppState) -> Result<()> {
|
||||
setpoint: effective_action.target_temperature,
|
||||
}, "automation.group").await.map(|value| !value.get("suppressed").and_then(Value::as_bool).unwrap_or(false))
|
||||
} else {
|
||||
match apply_automatic_device_action(state, &item.action_device_id, effective_action.clone()).await {
|
||||
match apply_automatic_device_action(
|
||||
state, &item.action_device_id, effective_action.clone(),
|
||||
item.flow_id.as_deref().zip(item.flow_node_id.as_deref()),
|
||||
).await {
|
||||
Ok(Some(_)) => Ok(true),
|
||||
Ok(None) => {
|
||||
state.log("info", "automation.blocked_by_fresh_ownership", &format!("Automation {} was suppressed after ownership changed", item.name), json!({
|
||||
@@ -1128,6 +1157,7 @@ async fn apply_flow_zone_action(
|
||||
state: &AppState, zone_id: &str, preset: Option<&str>, action: &DeviceCommand,
|
||||
heat_anticipation_c: Option<f64>, standby_offset_c: Option<f64>,
|
||||
simple_heat: Option<&SimpleHeatSettings>,
|
||||
flow_id: Option<&str>, flow_node_id: Option<&str>,
|
||||
) -> Result<bool, AppError> {
|
||||
let _schedule_guard = state.lock_schedule_operation().await;
|
||||
// Match the canonical thermostat ordering used by Web/HA zone control. This prevents
|
||||
@@ -1185,6 +1215,8 @@ async fn apply_flow_zone_action(
|
||||
zone.updated_at = Utc::now();
|
||||
zone.control_owner = "automation".into();
|
||||
zone.control_source = "automation.flow".into();
|
||||
zone.flow_control_id = flow_id.map(str::to_string);
|
||||
zone.flow_control_node_id = flow_node_id.map(str::to_string);
|
||||
zone.control_reason = "Visual Flow automation".into();
|
||||
let schedules = state.db.list_schedules()?;
|
||||
let runtime_settings = state.settings.read().await;
|
||||
|
||||
Reference in new issue
Block a user