This commit is contained in:
Mateusz Gruszczyński
2026-09-02 10:12:51 +02:00
parent 8180d22cef
commit e50c94b6a3
19 changed files with 323 additions and 108 deletions
+75 -1
View File
@@ -51,6 +51,62 @@ fn flow_condition_kind(kind: &str) -> bool {
fn flow_logic_kind(kind: &str) -> bool { matches!(kind, "logic_and" | "logic_or" | "logic_not") }
fn flow_action_kind(kind: &str) -> bool { matches!(kind, "zone_thermostat" | "device_action" | "group_action") }
fn shared_input_comparison_kind(kind: &str) -> bool {
matches!(kind,
"outdoor_temperature" | "device_temperature" | "zone_temperature" |
"ha_state" | "ha_numeric" | "ha_attribute" | "house_mode" |
"device_state" | "zone_state" | "group_state"
)
}
fn validate_shared_input_source(kind: &str, config: &Value, state: &AppState) -> Result<(), AppError> {
match kind {
"outdoor_temperature" | "house_mode" | "night_mode" => {}
"device_temperature" => {
let id = flow_string(config, "device_id").ok_or_else(|| AppError::BadRequest("shared device temperature input needs device".into()))?;
if state.db.get_device(&id)?.is_none() { return Err(AppError::BadRequest("shared Flow input references a missing device".into())); }
}
"zone_temperature" => {
let id = flow_string(config, "zone_id").ok_or_else(|| AppError::BadRequest("shared zone temperature input needs zone".into()))?;
if state.db.get_zone(&id)?.is_none() { return Err(AppError::BadRequest("shared Flow input references a missing zone".into())); }
}
"ha_state" | "ha_numeric" | "ha_available" => {
if flow_string(config, "entity_id").is_none() { return Err(AppError::BadRequest("shared Home Assistant input needs entity_id".into())); }
}
"ha_attribute" => {
if flow_string(config, "entity_id").is_none() || flow_string(config, "attribute").is_none() {
return Err(AppError::BadRequest("shared Home Assistant attribute input needs entity_id and attribute".into()));
}
}
"device_state" => {
let id = flow_string(config, "device_id").ok_or_else(|| AppError::BadRequest("shared device state input needs device".into()))?;
if state.db.get_device(&id)?.is_none() { return Err(AppError::BadRequest("shared Flow input references a missing device".into())); }
let field = flow_string(config, "field").ok_or_else(|| AppError::BadRequest("shared device state input needs a field".into()))?;
if !matches!(field.as_str(), "enabled" | "online" | "power" | "mode" | "fan_speed" | "swing_vertical" | "swing_horizontal" | "quiet" | "turbo" | "light" | "air" | "xfan" | "health" | "sleep") {
return Err(AppError::BadRequest("unsupported shared device state field".into()));
}
}
"zone_state" => {
let id = flow_string(config, "zone_id").ok_or_else(|| AppError::BadRequest("shared zone state input needs zone".into()))?;
if state.db.get_zone(&id)?.is_none() { return Err(AppError::BadRequest("shared Flow input references a missing zone".into())); }
let field = flow_string(config, "field").ok_or_else(|| AppError::BadRequest("shared zone state input needs a field".into()))?;
if !matches!(field.as_str(), "enabled" | "mode" | "active_preset" | "demand" | "control_owner" | "device_manual_override" | "local_thermostat_power") {
return Err(AppError::BadRequest("unsupported shared zone state field".into()));
}
}
"group_state" => {
let id = flow_string(config, "group_id").ok_or_else(|| AppError::BadRequest("shared group state input needs group".into()))?;
if state.db.get_group(&id)?.is_none() { return Err(AppError::BadRequest("shared Flow input references a missing group".into())); }
if flow_string(config, "field").as_deref() != Some("power_enabled") { return Err(AppError::BadRequest("unsupported shared group state field".into())); }
}
"constant" => {
if config.get("value").and_then(Value::as_bool).is_none() { return Err(AppError::BadRequest("shared constant input needs a boolean value".into())); }
}
_ => return Err(AppError::BadRequest(format!("unsupported shared Flow input kind: {kind}"))),
}
Ok(())
}
fn validate_flow_graph(input: &FlowInput) -> Result<(), AppError> {
if input.name.trim().is_empty() { return Err(AppError::BadRequest("flow name is required".into())); }
if input.nodes.is_empty() { return Err(AppError::BadRequest("flow needs at least one block".into())); }
@@ -221,7 +277,25 @@ fn validate_condition(condition: &crate::models::FlowCondition, state: &AppState
if condition.config.get("value").and_then(Value::as_bool).is_none() { return Err(AppError::BadRequest("constant block needs a boolean value".into())); }
}
"shared_input" => {
if flow_string(&condition.config, "input_id").is_none() { return Err(AppError::BadRequest("shared Flow input block needs input_id".into())); }
let input_id = flow_string(&condition.config, "input_id").ok_or_else(|| AppError::BadRequest("shared Flow input block needs input_id".into()))?;
let settings = state.db.load_runtime_settings()?.ok_or_else(|| AppError::BadRequest("runtime settings are unavailable".into()))?;
let shared = settings.home_assistant.flow_inputs.iter().find(|item| item.id == input_id)
.ok_or_else(|| AppError::BadRequest("shared Flow input block references a missing input".into()))?;
validate_shared_input_source(&shared.kind, &shared.config, state)?;
if shared_input_comparison_kind(&shared.kind) {
let operator = flow_string(&condition.config, "operator")
.ok_or_else(|| AppError::BadRequest("shared Flow value needs an operator in the Flow block".into()))?;
let mut config = shared.config.clone();
let map = config.as_object_mut().ok_or_else(|| AppError::BadRequest("shared Flow input config must be an object".into()))?;
map.insert("operator".into(), Value::String(operator));
map.insert("value".into(), condition.config.get("value").cloned().unwrap_or(Value::Null));
let resolved = crate::models::FlowCondition {
id: condition.id.clone(), kind: shared.kind.clone(), config, inputs: Vec::new(),
};
validate_condition(&resolved, state)?;
} else if flow_string(&condition.config, "operator").is_some() || condition.config.get("value").is_some() {
return Err(AppError::BadRequest("this shared Flow input is already boolean and does not accept a comparison".into()));
}
}
_ => {}
}
+10 -7
View File
@@ -120,13 +120,16 @@ fn validate_flow_shared_inputs(settings: &mut RuntimeSettings, state: &AppState)
"night_mode" | "constant") {
return Err(AppError::BadRequest(format!("unsupported shared Flow input kind: {}", item.kind)));
}
let condition = crate::models::FlowCondition {
id: item.id.clone(),
kind: item.kind.clone(),
config: item.config.clone(),
inputs: Vec::new(),
};
validate_condition(&condition, state)?;
if !item.config.is_object() {
return Err(AppError::BadRequest("shared Flow input config must be an object".into()));
}
if item.config.get("operator").is_some() {
return Err(AppError::BadRequest("shared Flow inputs are value sources; operator belongs to the Flow block".into()));
}
if shared_input_comparison_kind(&item.kind) && item.config.get("value").is_some() {
return Err(AppError::BadRequest("shared Flow inputs are value sources; comparison value belongs to the Flow block".into()));
}
validate_shared_input_source(&item.kind, &item.config, state)?;
}
Ok(())
}