v0.15.8
This commit is contained in:
+200
-28
@@ -53,6 +53,109 @@ fn flow_u8(config: &Value, key: &str) -> Option<u8> {
|
||||
fn flow_bool(config: &Value, key: &str) -> Option<bool> {
|
||||
config.get(key).and_then(Value::as_bool)
|
||||
}
|
||||
|
||||
fn flow_referenced_shared_input_ids(
|
||||
nodes: &[crate::models::FlowNode],
|
||||
) -> std::collections::HashSet<String> {
|
||||
nodes
|
||||
.iter()
|
||||
.filter(|node| node.kind == "shared_input")
|
||||
.filter_map(|node| flow_string(&node.config, "input_id"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn same_shared_input_source(
|
||||
left: &crate::models::FlowSharedInput,
|
||||
right: &crate::models::FlowSharedInput,
|
||||
) -> bool {
|
||||
left.kind == right.kind && left.config == right.config
|
||||
}
|
||||
|
||||
fn prepare_imported_shared_inputs(
|
||||
input: &mut FlowInput,
|
||||
imported: Vec<crate::models::FlowSharedInput>,
|
||||
current: &[crate::models::FlowSharedInput],
|
||||
) -> Result<(Vec<crate::models::FlowSharedInput>, bool), AppError> {
|
||||
let mut imported_ids = std::collections::HashSet::new();
|
||||
let mut normalized_imported = Vec::with_capacity(imported.len());
|
||||
for mut item in imported {
|
||||
item.id = item.id.trim().chars().take(120).collect();
|
||||
item.name = item.name.trim().chars().take(100).collect();
|
||||
item.kind = item.kind.trim().to_string();
|
||||
if item.id.is_empty() || item.name.is_empty() {
|
||||
return Err(AppError::BadRequest(
|
||||
"imported shared Flow input requires id and name".into(),
|
||||
));
|
||||
}
|
||||
if !imported_ids.insert(item.id.clone()) {
|
||||
return Err(AppError::BadRequest(
|
||||
"imported shared Flow input IDs must be unique".into(),
|
||||
));
|
||||
}
|
||||
normalized_imported.push(item);
|
||||
}
|
||||
|
||||
let referenced_import_ids = flow_referenced_shared_input_ids(&input.nodes);
|
||||
let mut next = current.to_vec();
|
||||
let mut remap = std::collections::HashMap::<String, String>::new();
|
||||
let mut changed = false;
|
||||
|
||||
for item in normalized_imported
|
||||
.into_iter()
|
||||
.filter(|item| referenced_import_ids.contains(&item.id))
|
||||
{
|
||||
if let Some(existing) = next.iter().find(|existing| existing.id == item.id) {
|
||||
if same_shared_input_source(existing, &item) {
|
||||
remap.insert(item.id.clone(), existing.id.clone());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(existing) = next
|
||||
.iter()
|
||||
.find(|existing| same_shared_input_source(existing, &item))
|
||||
{
|
||||
remap.insert(item.id.clone(), existing.id.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
let original_id = item.id.clone();
|
||||
let mut to_add = item;
|
||||
if next.iter().any(|existing| existing.id == to_add.id) {
|
||||
to_add.id = format!("shared-{}", Uuid::new_v4());
|
||||
}
|
||||
remap.insert(original_id, to_add.id.clone());
|
||||
next.push(to_add);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
for node in &mut input.nodes {
|
||||
if node.kind != "shared_input" {
|
||||
continue;
|
||||
}
|
||||
let Some(input_id) = flow_string(&node.config, "input_id") else {
|
||||
continue;
|
||||
};
|
||||
if let Some(mapped) = remap.get(&input_id) {
|
||||
if mapped != &input_id {
|
||||
let config = node.config.as_object_mut().ok_or_else(|| {
|
||||
AppError::BadRequest("shared Flow input block config must be an object".into())
|
||||
})?;
|
||||
config.insert("input_id".into(), Value::String(mapped.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for input_id in flow_referenced_shared_input_ids(&input.nodes) {
|
||||
if !next.iter().any(|item| item.id == input_id) {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"shared Flow input block references missing input {input_id}; export the Flow again with shared input definitions or create the input first"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((next, changed))
|
||||
}
|
||||
fn flow_louver_position(config: &Value, key: &str) -> Option<u8> {
|
||||
match config.get(key) {
|
||||
Some(Value::Bool(value)) => Some(u8::from(*value)),
|
||||
@@ -604,6 +707,7 @@ fn validate_text_comparison(config: &Value) -> Result<(), AppError> {
|
||||
fn validate_condition(
|
||||
condition: &crate::models::FlowCondition,
|
||||
state: &AppState,
|
||||
shared_inputs_override: Option<&[crate::models::FlowSharedInput]>,
|
||||
) -> Result<(), AppError> {
|
||||
match condition.kind.as_str() {
|
||||
"weekday" => {
|
||||
@@ -1032,20 +1136,25 @@ fn validate_condition(
|
||||
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(),
|
||||
)
|
||||
})?;
|
||||
let shared = if let Some(inputs) = shared_inputs_override {
|
||||
inputs.iter().find(|item| item.id == input_id).cloned()
|
||||
} else {
|
||||
state
|
||||
.db
|
||||
.load_runtime_settings()?
|
||||
.and_then(|settings| {
|
||||
settings
|
||||
.home_assistant
|
||||
.flow_inputs
|
||||
.into_iter()
|
||||
.find(|item| item.id == input_id)
|
||||
})
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
AppError::BadRequest(format!(
|
||||
"shared Flow input block references missing input {input_id}"
|
||||
))
|
||||
})?;
|
||||
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(|| {
|
||||
@@ -1072,7 +1181,7 @@ fn validate_condition(
|
||||
config,
|
||||
inputs: Vec::new(),
|
||||
};
|
||||
validate_condition(&resolved, state)?;
|
||||
validate_condition(&resolved, state, shared_inputs_override)?;
|
||||
} else if flow_string(&condition.config, "operator").is_some()
|
||||
|| condition.config.get("value").is_some()
|
||||
{
|
||||
@@ -1104,6 +1213,7 @@ fn validate_flow_comparison(config: &Value) -> Result<(), AppError> {
|
||||
fn compile_flow(
|
||||
state: &AppState,
|
||||
mut flow: crate::models::Flow,
|
||||
shared_inputs_override: Option<&[crate::models::FlowSharedInput]>,
|
||||
) -> Result<(crate::models::Flow, Vec<Schedule>, Vec<Automation>), AppError> {
|
||||
let mut schedules = Vec::new();
|
||||
let mut automations = Vec::new();
|
||||
@@ -1137,7 +1247,7 @@ fn compile_flow(
|
||||
.iter()
|
||||
.filter(|condition| flow_condition_kind(&condition.kind))
|
||||
{
|
||||
validate_condition(condition, state)?;
|
||||
validate_condition(condition, state, shared_inputs_override)?;
|
||||
}
|
||||
let schedule_leaves: Vec<_> = conditions
|
||||
.iter()
|
||||
@@ -1572,7 +1682,7 @@ async fn create_flow(
|
||||
let (flow, schedules, automations) = if flow.draft {
|
||||
(prepare_draft_flow(flow), vec![], vec![])
|
||||
} else {
|
||||
compile_flow(&state, flow)?
|
||||
compile_flow(&state, flow, None)?
|
||||
};
|
||||
state
|
||||
.db
|
||||
@@ -1629,7 +1739,7 @@ async fn update_flow(
|
||||
let (flow, schedules, automations) = if flow.draft {
|
||||
(prepare_draft_flow(flow), vec![], vec![])
|
||||
} else {
|
||||
compile_flow(&state, flow)?
|
||||
compile_flow(&state, flow, None)?
|
||||
};
|
||||
state
|
||||
.db
|
||||
@@ -1689,10 +1799,22 @@ async fn export_flow(
|
||||
.db
|
||||
.get_flow(&id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("flow {id}")))?;
|
||||
let referenced = flow_referenced_shared_input_ids(&flow.nodes);
|
||||
let shared_inputs: Vec<_> = state
|
||||
.settings
|
||||
.read()
|
||||
.await
|
||||
.home_assistant
|
||||
.flow_inputs
|
||||
.iter()
|
||||
.filter(|item| referenced.contains(&item.id))
|
||||
.cloned()
|
||||
.collect();
|
||||
Ok(Json(json!({
|
||||
"format": "gree-controller-flow",
|
||||
"version": 1,
|
||||
"exported_at": Utc::now(),
|
||||
"shared_inputs": shared_inputs,
|
||||
"flow": {
|
||||
"name": flow.name,
|
||||
"enabled": flow.enabled,
|
||||
@@ -1715,28 +1837,74 @@ async fn import_flow(
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(version) = document.get("version").and_then(Value::as_u64) {
|
||||
if !matches!(version, 1 | 2) {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"unsupported Flow import version {version}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
let imported_shared_inputs: Vec<crate::models::FlowSharedInput> = document
|
||||
.get("shared_inputs")
|
||||
.cloned()
|
||||
.map(serde_json::from_value)
|
||||
.transpose()
|
||||
.map_err(|err| AppError::BadRequest(format!("invalid shared Flow inputs: {err}")))?
|
||||
.unwrap_or_default();
|
||||
let payload = document.get("flow").cloned().unwrap_or(document);
|
||||
let mut input: FlowInput = serde_json::from_value(payload)
|
||||
.map_err(|err| AppError::BadRequest(format!("invalid Flow import: {err}")))?;
|
||||
input.expected_revision = None;
|
||||
|
||||
let _configuration_guard = state.lock_configuration_operation().await;
|
||||
let _automation_guard = state.lock_automation_operation().await;
|
||||
let _schedule_guard = state.lock_schedule_operation().await;
|
||||
let _cycle_guard = state.lock_zone_control_cycle().await;
|
||||
|
||||
let mut next_settings = state.settings.read().await.clone();
|
||||
let (flow_inputs, shared_inputs_changed) = prepare_imported_shared_inputs(
|
||||
&mut input,
|
||||
imported_shared_inputs,
|
||||
&next_settings.home_assistant.flow_inputs,
|
||||
)?;
|
||||
next_settings.home_assistant.flow_inputs = flow_inputs;
|
||||
validate_flow_shared_inputs(&mut next_settings.home_assistant, &state)?;
|
||||
|
||||
if input.draft {
|
||||
validate_flow_draft_graph(&input)?;
|
||||
} else {
|
||||
validate_flow_graph(&input)?;
|
||||
}
|
||||
let _configuration_guard = state.lock_configuration_operation().await;
|
||||
let _automation_guard = state.lock_automation_operation().await;
|
||||
let _schedule_guard = state.lock_schedule_operation().await;
|
||||
let _cycle_guard = state.lock_zone_control_cycle().await;
|
||||
|
||||
let flow = flow_from_input(Uuid::new_v4().to_string(), input, Utc::now(), 1);
|
||||
let (flow, schedules, automations) = if flow.draft {
|
||||
(prepare_draft_flow(flow), vec![], vec![])
|
||||
} else {
|
||||
compile_flow(&state, flow)?
|
||||
compile_flow(
|
||||
&state,
|
||||
flow,
|
||||
Some(&next_settings.home_assistant.flow_inputs),
|
||||
)?
|
||||
};
|
||||
state
|
||||
.db
|
||||
.replace_flow_outputs(&flow, &schedules, &automations)?;
|
||||
|
||||
if shared_inputs_changed {
|
||||
state.db.replace_flow_outputs_with_runtime_settings(
|
||||
&flow,
|
||||
&schedules,
|
||||
&automations,
|
||||
&next_settings,
|
||||
)?;
|
||||
*state.settings.write().await = next_settings.clone();
|
||||
let payload = home_assistant_settings(&next_settings);
|
||||
state.broadcast(
|
||||
"settings.home_assistant.updated",
|
||||
serde_json::to_value(&payload)?,
|
||||
);
|
||||
} else {
|
||||
state
|
||||
.db
|
||||
.replace_flow_outputs(&flow, &schedules, &automations)?;
|
||||
}
|
||||
for zone_id in schedules
|
||||
.iter()
|
||||
.map(|s| s.zone_id.as_str())
|
||||
@@ -1748,7 +1916,11 @@ async fn import_flow(
|
||||
"info",
|
||||
"flow.imported",
|
||||
&format!("Imported Flow {}", flow.name),
|
||||
json!({"flow_id": flow.id, "revision": flow.revision}),
|
||||
json!({
|
||||
"flow_id": flow.id,
|
||||
"revision": flow.revision,
|
||||
"shared_inputs_imported": shared_inputs_changed
|
||||
}),
|
||||
);
|
||||
state.broadcast("flow.created", serde_json::to_value(&flow)?);
|
||||
state.wake_zone_control();
|
||||
@@ -1896,7 +2068,7 @@ async fn simulate_flow(
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
let (compiled, schedules, automations) = compile_flow(&state, preview)?;
|
||||
let (compiled, schedules, automations) = compile_flow(&state, preview, None)?;
|
||||
let devices = state.db.list_devices()?;
|
||||
let mut actions = Vec::new();
|
||||
for action in input
|
||||
|
||||
@@ -45,6 +45,50 @@ impl Db {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn replace_flow_outputs_with_runtime_settings(
|
||||
&self,
|
||||
flow: &Flow,
|
||||
schedules: &[Schedule],
|
||||
automations: &[Automation],
|
||||
settings: &RuntimeSettings,
|
||||
) -> Result<()> {
|
||||
let mut conn = self.lock()?;
|
||||
let tx = conn.transaction()?;
|
||||
tx.execute(queries::DELETE_SCHEDULES_BY_FLOW_ID, [flow.id.as_str()])?;
|
||||
tx.execute(queries::DELETE_AUTOMATIONS_BY_FLOW_ID, [flow.id.as_str()])?;
|
||||
for schedule in schedules {
|
||||
let payload = Self::to_json(schedule)?;
|
||||
tx.execute(
|
||||
queries::UPSERT_SCHEDULE,
|
||||
params![
|
||||
schedule.id,
|
||||
schedule.zone_id,
|
||||
payload,
|
||||
schedule.updated_at.to_rfc3339()
|
||||
],
|
||||
)?;
|
||||
}
|
||||
for item in automations {
|
||||
let payload = Self::to_json(item)?;
|
||||
tx.execute(
|
||||
queries::UPSERT_AUTOMATION,
|
||||
params![item.id, payload, item.updated_at.to_rfc3339()],
|
||||
)?;
|
||||
}
|
||||
let flow_payload = Self::to_json(flow)?;
|
||||
tx.execute(
|
||||
queries::UPSERT_FLOW,
|
||||
params![flow.id, flow_payload, flow.updated_at.to_rfc3339()],
|
||||
)?;
|
||||
let settings_payload = Self::to_json(settings)?;
|
||||
tx.execute(
|
||||
queries::UPSERT_RUNTIME_SETTINGS,
|
||||
params![settings_payload, Utc::now().to_rfc3339()],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_flow(&self, id: &str) -> Result<bool> {
|
||||
let mut conn = self.lock()?;
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
Reference in New Issue
Block a user