v0.11.1
This commit is contained in:
+46
-2
@@ -43,7 +43,7 @@ fn generated_flow_name(flow_id: &str, action_node_id: &str) -> String {
|
||||
|
||||
fn flow_condition_kind(kind: &str) -> bool {
|
||||
matches!(kind,
|
||||
"weekday" | "time_range" | "date_range" | "cron_trigger" | "stable_for" | "delay" | "rolling_stat" | "oscillates" | "outdoor_temperature" | "device_temperature" |
|
||||
"weekday" | "time_range" | "date_range" | "cron_trigger" | "stable_for" | "delay" | "state_duration" | "on_change" | "rate_limit" | "rolling_stat" | "oscillates" | "outdoor_temperature" | "device_temperature" |
|
||||
"zone_temperature" | "ha_state" | "ha_numeric" | "ha_attribute" | "ha_available" | "house_mode" |
|
||||
"device_state" | "zone_state" | "group_state" | "night_mode" | "constant" | "shared_input"
|
||||
)
|
||||
@@ -136,6 +136,24 @@ fn validate_flow_graph(input: &FlowInput) -> Result<(), AppError> {
|
||||
return Err(AppError::BadRequest("Flow connection has an unsupported target".into()));
|
||||
}
|
||||
}
|
||||
for node in input.nodes.iter().filter(|node| node.kind == "rate_limit") {
|
||||
let targets = input.edges.iter().filter(|edge| edge.from == node.id)
|
||||
.filter_map(|edge| input.nodes.iter().find(|target| target.id == edge.to))
|
||||
.collect::<Vec<_>>();
|
||||
if targets.is_empty() || targets.iter().any(|target| !flow_action_kind(&target.kind)) {
|
||||
return Err(AppError::BadRequest("rate-limit block must be placed directly before an action".into()));
|
||||
}
|
||||
}
|
||||
|
||||
for node in input.nodes.iter().filter(|node| node.kind == "on_change" && flow_string(&node.config, "mode").as_deref() == Some("value")) {
|
||||
let sources = input.edges.iter().filter(|edge| edge.to == node.id)
|
||||
.filter_map(|edge| input.nodes.iter().find(|source| source.id == edge.from))
|
||||
.collect::<Vec<_>>();
|
||||
if sources.len() != 1 || sources.iter().any(|source| flow_logic_kind(&source.kind) || matches!(source.kind.as_str(), "stable_for" | "delay" | "state_duration" | "on_change" | "rate_limit" | "rolling_stat" | "oscillates")) {
|
||||
return Err(AppError::BadRequest("on-change value mode needs one direct source/condition input".into()));
|
||||
}
|
||||
}
|
||||
|
||||
// Reject cycles. Flow is deliberately a DAG: finite evaluation, deterministic topological order,
|
||||
// and no hidden state machine semantics unless a dedicated stateful block is introduced later.
|
||||
let mut outgoing = std::collections::HashMap::<String, Vec<String>>::new();
|
||||
@@ -230,6 +248,32 @@ fn validate_condition(condition: &crate::models::FlowCondition, state: &AppState
|
||||
if seconds == 0 || seconds > 604800 { return Err(AppError::BadRequest("delay must be between 1 second and 7 days".into())); }
|
||||
if condition.inputs.len() != 1 { return Err(AppError::BadRequest("delay block needs exactly one input".into())); }
|
||||
}
|
||||
"state_duration" => {
|
||||
let min_seconds = condition.config.get("min_seconds").and_then(Value::as_u64).unwrap_or(0);
|
||||
let max_seconds = condition.config.get("max_seconds").and_then(Value::as_u64);
|
||||
if min_seconds > 604800 || max_seconds.is_some_and(|value| value > 604800) {
|
||||
return Err(AppError::BadRequest("state duration must be between 0 seconds and 7 days".into()));
|
||||
}
|
||||
if max_seconds.is_some_and(|value| value < min_seconds) {
|
||||
return Err(AppError::BadRequest("state duration maximum must be greater than or equal to minimum".into()));
|
||||
}
|
||||
if min_seconds == 0 && max_seconds.is_none() {
|
||||
return Err(AppError::BadRequest("state duration needs a minimum or maximum duration".into()));
|
||||
}
|
||||
if condition.inputs.len() != 1 { return Err(AppError::BadRequest("state duration block needs exactly one input".into())); }
|
||||
}
|
||||
"on_change" => {
|
||||
let mode = flow_string(&condition.config, "mode").unwrap_or_else(|| "result".into());
|
||||
if !matches!(mode.as_str(), "result" | "value") { return Err(AppError::BadRequest("on-change mode must be result or value".into())); }
|
||||
if condition.inputs.len() != 1 { return Err(AppError::BadRequest("on-change block needs exactly one input".into())); }
|
||||
}
|
||||
"rate_limit" => {
|
||||
let max_count = condition.config.get("max_count").and_then(Value::as_u64).unwrap_or(0);
|
||||
let period_seconds = condition.config.get("period_seconds").and_then(Value::as_u64).unwrap_or(0);
|
||||
if !(1..=1000).contains(&max_count) { return Err(AppError::BadRequest("rate limit max_count must be between 1 and 1000".into())); }
|
||||
if !(1..=2678400).contains(&period_seconds) { return Err(AppError::BadRequest("rate limit period must be between 1 second and 31 days".into())); }
|
||||
if condition.inputs.len() != 1 { return Err(AppError::BadRequest("rate-limit block needs exactly one input".into())); }
|
||||
}
|
||||
"rolling_stat" => {
|
||||
let source = flow_string(&condition.config, "source").unwrap_or_default();
|
||||
if !matches!(source.as_str(), "outdoor_temperature" | "device_temperature" | "zone_temperature" | "ha_numeric") { return Err(AppError::BadRequest("rolling statistic has an unsupported source".into())); }
|
||||
@@ -409,7 +453,7 @@ fn compile_flow(state: &AppState, mut flow: crate::models::Flow) -> Result<(crat
|
||||
runtime.retain(|node_id, _| {
|
||||
let Some(previous) = previous_conditions.get(node_id.as_str()) else { return false; };
|
||||
let Some(current) = current_conditions.get(node_id.as_str()) else { return false; };
|
||||
matches!(current.kind.as_str(), "stable_for" | "delay" | "rolling_stat" | "oscillates")
|
||||
matches!(current.kind.as_str(), "stable_for" | "delay" | "state_duration" | "on_change" | "rate_limit" | "rolling_stat" | "oscillates")
|
||||
&& previous.kind == current.kind
|
||||
&& previous.config == current.config
|
||||
&& previous.inputs == current.inputs
|
||||
|
||||
Reference in New Issue
Block a user