v0.15.2
This commit is contained in:
+114
-17
@@ -63,6 +63,66 @@ fn flow_louver_position(config: &Value, key: &str) -> Option<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
fn flow_device_feature_command(config: &Value) -> Result<DeviceCommand, AppError> {
|
||||
let feature = flow_string(config, "feature")
|
||||
.ok_or_else(|| AppError::BadRequest("device feature action needs a feature".into()))?;
|
||||
let mut command = DeviceCommand::default();
|
||||
match feature.as_str() {
|
||||
"power" => {
|
||||
command.power = Some(flow_bool(config, "value").ok_or_else(|| {
|
||||
AppError::BadRequest("device feature power needs a boolean value".into())
|
||||
})?);
|
||||
}
|
||||
"mode" => {
|
||||
command.mode = Some(flow_string(config, "value").ok_or_else(|| {
|
||||
AppError::BadRequest("device feature mode needs a value".into())
|
||||
})?);
|
||||
}
|
||||
"target_temperature" => {
|
||||
command.target_temperature = Some(flow_f64(config, "value").ok_or_else(|| {
|
||||
AppError::BadRequest("device feature target temperature needs a numeric value".into())
|
||||
})?);
|
||||
}
|
||||
"fan_speed" => {
|
||||
command.fan_speed = Some(flow_u8(config, "value").ok_or_else(|| {
|
||||
AppError::BadRequest("device feature fan speed needs an integer value".into())
|
||||
})?);
|
||||
}
|
||||
"swing_vertical" => {
|
||||
command.swing_vertical = Some(flow_louver_position(config, "value").ok_or_else(|| {
|
||||
AppError::BadRequest("device feature vertical louver needs a position".into())
|
||||
})?);
|
||||
}
|
||||
"swing_horizontal" => {
|
||||
command.swing_horizontal = Some(flow_louver_position(config, "value").ok_or_else(|| {
|
||||
AppError::BadRequest("device feature horizontal louver needs a position".into())
|
||||
})?);
|
||||
}
|
||||
"quiet" | "turbo" | "light" | "air" | "xfan" | "health" | "sleep" => {
|
||||
let value = flow_bool(config, "value").ok_or_else(|| {
|
||||
AppError::BadRequest("device feature action needs an on/off value".into())
|
||||
})?;
|
||||
match feature.as_str() {
|
||||
"quiet" => command.quiet = Some(value),
|
||||
"turbo" => command.turbo = Some(value),
|
||||
"light" => command.light = Some(value),
|
||||
"air" => command.air = Some(value),
|
||||
"xfan" => command.xfan = Some(value),
|
||||
"health" => command.health = Some(value),
|
||||
"sleep" => command.sleep = Some(value),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"unsupported device feature action: {feature}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
engine::validate_command(&command)?;
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
fn generated_flow_name(flow_id: &str, action_node_id: &str) -> String {
|
||||
let digest = Sha256::digest(format!("{flow_id}:{action_node_id}").as_bytes());
|
||||
format!("flow-{}", URL_SAFE_NO_PAD.encode(&digest[..12]))
|
||||
@@ -104,7 +164,11 @@ fn flow_logic_kind(kind: &str) -> bool {
|
||||
fn flow_action_kind(kind: &str) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
"zone_thermostat" | "device_action" | "group_action" | "ha_service_action"
|
||||
"zone_thermostat"
|
||||
| "device_action"
|
||||
| "device_feature_action"
|
||||
| "group_action"
|
||||
| "ha_service_action"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1332,6 +1396,18 @@ fn compile_flow(
|
||||
return Err(AppError::BadRequest("device action cannot be empty".into()));
|
||||
}
|
||||
}
|
||||
"device_feature_action" => {
|
||||
let id = flow_string(&action_node.config, "device_id").ok_or_else(|| {
|
||||
AppError::BadRequest("device feature action needs a device".into())
|
||||
})?;
|
||||
if !devices.iter().any(|d| d.id == id) {
|
||||
return Err(AppError::BadRequest(
|
||||
"device feature action references a missing device".into(),
|
||||
));
|
||||
}
|
||||
item.action_device_id = id;
|
||||
item.action = flow_device_feature_command(&action_node.config)?;
|
||||
}
|
||||
"ha_service_action" => {
|
||||
let domain = flow_string(&action_node.config, "domain").ok_or_else(|| {
|
||||
AppError::BadRequest("Home Assistant action needs a domain".into())
|
||||
@@ -1708,7 +1784,7 @@ fn dry_run_block_reason(
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
"device_action" => {
|
||||
"device_action" | "device_feature_action" => {
|
||||
let Some(device_id) = flow_string(&action.config, "device_id") else {
|
||||
return Ok(Some("missing_device".into()));
|
||||
};
|
||||
@@ -1737,25 +1813,30 @@ fn dry_run_block_reason(
|
||||
}) {
|
||||
return Ok(Some("temporary_quick_thermostat".into()));
|
||||
}
|
||||
let command = if action.kind == "device_feature_action" {
|
||||
flow_device_feature_command(&action.config)?
|
||||
} else {
|
||||
let mut command = DeviceCommand::default();
|
||||
command.power = flow_bool(&action.config, "power");
|
||||
command.mode = flow_string(&action.config, "mode");
|
||||
command.target_temperature = flow_f64(&action.config, "target_temperature");
|
||||
command.fan_speed = flow_u8(&action.config, "fan_speed");
|
||||
command.swing_vertical = flow_louver_position(&action.config, "swing_vertical");
|
||||
command.swing_horizontal = flow_louver_position(&action.config, "swing_horizontal");
|
||||
command.quiet = flow_bool(&action.config, "quiet");
|
||||
command.turbo = flow_bool(&action.config, "turbo");
|
||||
command.light = flow_bool(&action.config, "light");
|
||||
command.air = flow_bool(&action.config, "air");
|
||||
command.xfan = flow_bool(&action.config, "xfan");
|
||||
command.health = flow_bool(&action.config, "health");
|
||||
command.sleep = flow_bool(&action.config, "sleep");
|
||||
command
|
||||
};
|
||||
if zones.iter().any(|z| z.device_id == device_id && !z.enabled)
|
||||
&& flow_bool(&action.config, "power") != Some(true)
|
||||
&& command.power != Some(true)
|
||||
{
|
||||
return Ok(Some("zone_disabled".into()));
|
||||
}
|
||||
let mut command = DeviceCommand::default();
|
||||
command.power = flow_bool(&action.config, "power");
|
||||
command.mode = flow_string(&action.config, "mode");
|
||||
command.target_temperature = flow_f64(&action.config, "target_temperature");
|
||||
command.fan_speed = flow_u8(&action.config, "fan_speed");
|
||||
command.swing_vertical = flow_louver_position(&action.config, "swing_vertical");
|
||||
command.swing_horizontal = flow_louver_position(&action.config, "swing_horizontal");
|
||||
command.quiet = flow_bool(&action.config, "quiet");
|
||||
command.turbo = flow_bool(&action.config, "turbo");
|
||||
command.light = flow_bool(&action.config, "light");
|
||||
command.air = flow_bool(&action.config, "air");
|
||||
command.xfan = flow_bool(&action.config, "xfan");
|
||||
command.health = flow_bool(&action.config, "health");
|
||||
command.sleep = flow_bool(&action.config, "sleep");
|
||||
if zones.iter().any(|z| z.device_id == device_id)
|
||||
&& engine::automation_action_conflicts_with_thermostat(&command)
|
||||
{
|
||||
@@ -1950,4 +2031,20 @@ mod flow_draft_tests {
|
||||
assert!(flow.compiled_schedule_ids.is_empty());
|
||||
assert!(flow.compiled_automation_ids.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn device_feature_action_builds_only_selected_field() {
|
||||
let command = flow_device_feature_command(&json!({"feature": "light", "value": true}))
|
||||
.expect("valid feature action");
|
||||
assert_eq!(command.light, Some(true));
|
||||
assert!(command.power.is_none());
|
||||
assert!(command.mode.is_none());
|
||||
assert!(command.swing_vertical.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn device_feature_action_validates_louver_range() {
|
||||
let result = flow_device_feature_command(&json!({"feature": "swing_vertical", "value": 99}));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user