This commit is contained in:
Mateusz Gruszczyński committed 2026-09-23 09:24:15 +02:00
1 parent 0148daf410
commit f08613a254
23 files changed
+290 -70

No files matched your search

+13 -6
View File
@@ -26,6 +26,7 @@ async fn run_automations(state: &AppState) -> Result<()> {
for mut item in automations {
if !item.enabled { continue; }
let ready = automation_ready(&item);
let mut trigger_trace = Vec::<Value>::new();
let should_fire = match item.trigger_kind.as_str() {
// Stateful Flow blocks must keep observing while an action is in cooldown. Otherwise
// stable/change-duration state could silently span an unobserved interval.
@@ -34,7 +35,10 @@ async fn run_automations(state: &AppState) -> Result<()> {
let result = flow_conditions_match(state, &devices, &mut item).await;
if item.flow_runtime != before_runtime { state.db.save_automation(&item)?; }
match result {
Ok(value) => value,
Ok((value, trace)) => {
trigger_trace = trace;
value
},
Err(err) => {
state.log("warn", "flow.condition_error", &format!("Flow automation {} condition evaluation failed: {}", item.name, err), json!({"automation_id": item.id, "flow_id": item.flow_id, "flow_node_id": item.flow_node_id}));
false
@@ -168,6 +172,7 @@ async fn run_automations(state: &AppState) -> Result<()> {
flow_acknowledge_change_gates(&mut item);
flow_record_rate_limited_execution(&mut item, fired_at.clone());
item.last_fired_at = Some(fired_at);
item.last_trigger_trace = trigger_trace.clone();
item.updated_at = Utc::now();
state.db.save_automation(&item)?;
// Keep connected clients in sync with runtime metadata such as last_fired_at.
@@ -191,6 +196,7 @@ async fn run_automations(state: &AppState) -> Result<()> {
// A failed action is still an execution attempt. Apply the configured cooldown
// so an offline/disabled target cannot be hammered on every automation cycle.
item.last_fired_at = Some(Utc::now());
item.last_trigger_trace = trigger_trace.clone();
item.updated_at = Utc::now();
state.db.save_automation(&item)?;
state.broadcast("automation.updated", serde_json::to_value(&item)?);
@@ -704,7 +710,7 @@ pub async fn evaluate_flow_conditions_trace(
for condition in conditions {
let (matched, actual) = flow_leaf_observation(state, devices, &zones, outdoor_temperature, condition, &now, &settings, overrides).await?;
final_value &= matched;
trace.push(json!({"node_id": condition.id, "kind": condition.kind, "matched": matched, "actual": actual, "expected": condition.config.get("value")}));
trace.push(json!({"node_id": condition.id, "kind": condition.kind, "matched": matched, "actual": actual, "expected": condition.config.get("value"), "config": condition.config}));
}
return Ok((final_value, trace));
}
@@ -875,23 +881,24 @@ pub async fn evaluate_flow_conditions_trace(
"matched": matched,
"actual": actual,
"expected": expected,
"config": condition.config,
"inputs": condition.inputs
}));
}
Ok((final_id.and_then(|id| values.get(&id).copied()).unwrap_or(false), trace))
}
async fn flow_conditions_match(state: &AppState, devices: &[Device], item: &mut Automation) -> Result<bool, AppError> {
async fn flow_conditions_match(state: &AppState, devices: &[Device], item: &mut Automation) -> Result<(bool, Vec<Value>), AppError> {
let overrides = HashMap::new();
let conditions = item.flow_conditions.clone();
let now = Local::now();
let (matched, _) = evaluate_flow_conditions_trace(state, devices, &conditions, now.clone(), &overrides, Some(&mut item.flow_runtime)).await?;
let (matched, trace) = evaluate_flow_conditions_trace(state, devices, &conditions, now.clone(), &overrides, Some(&mut item.flow_runtime)).await?;
if matched && conditions.iter().any(|condition| condition.kind == "cron_trigger") {
if let Some(last) = item.last_fired_at.as_ref().map(|value| value.with_timezone(&Local)) {
if last.year() == now.year() && last.ordinal() == now.ordinal() && last.hour() == now.hour() && last.minute() == now.minute() { return Ok(false); }
if last.year() == now.year() && last.ordinal() == now.ordinal() && last.hour() == now.hour() && last.minute() == now.minute() { return Ok((false, trace)); }
}
}
Ok(matched)
Ok((matched, trace))
}
fn flow_condition_kind_runtime(kind: &str) -> bool {