diff --git a/apps/android/app/src/main/java/video/crumb/app/data/HaModels.kt b/apps/android/app/src/main/java/video/crumb/app/data/HaModels.kt index 1d1ead87..a0f6714d 100644 --- a/apps/android/app/src/main/java/video/crumb/app/data/HaModels.kt +++ b/apps/android/app/src/main/java/video/crumb/app/data/HaModels.kt @@ -120,6 +120,7 @@ fun haPrimaryAction(domain: String): String? = when (domain) { "light", "switch", "fan", "siren" -> "toggle" "button", "input_button" -> "press" "scene", "script" -> "turn_on" + "automation" -> "trigger" else -> null } @@ -188,6 +189,7 @@ fun haFullActions(domain: String): List = when (domain) { "button", "input_button" -> listOf(HaAction("press", "Press")) "scene" -> listOf(HaAction("turn_on", "Activate")) "script" -> listOf(HaAction("turn_on", "Run")) + "automation" -> listOf(HaAction("trigger", "Trigger")) else -> emptyList() } diff --git a/apps/desktop-flutter/lib/ui/ha_overlay/ha_actions.dart b/apps/desktop-flutter/lib/ui/ha_overlay/ha_actions.dart index 3914707c..b3086776 100644 --- a/apps/desktop-flutter/lib/ui/ha_overlay/ha_actions.dart +++ b/apps/desktop-flutter/lib/ui/ha_overlay/ha_actions.dart @@ -13,6 +13,7 @@ // lock -> lock / unlock // button, input_button -> press // scene, script -> turn_on +// automation -> trigger // // Anything else (binary_sensor, sensor, an unknown domain from a newer HA) // yields NO actions, so the card stays read-only exactly as it is today. @@ -128,6 +129,13 @@ const List _kScriptActions = [ HaControlAction(action: 'turn_on', label: 'Run', icon: Icons.play_arrow), ]; +/// automation — HA's service is `automation.trigger`, which fires the +/// automation's actions immediately (skipping its trigger conditions). "Trigger" +/// is what it reads as on the badge. +const List _kAutomationActions = [ + HaControlAction(action: 'trigger', label: 'Trigger', icon: Icons.bolt), +]; + /// The actions the server will accept for an entity in [domain]; empty for /// every domain that has no control path (the card then renders read-only). List haActionsForDomain(String domain) { @@ -148,6 +156,8 @@ List haActionsForDomain(String domain) { return _kSceneActions; case 'script': return _kScriptActions; + case 'automation': + return _kAutomationActions; default: return const []; } @@ -184,6 +194,7 @@ List haAllActionsForDomain(String domain) { /// light / switch / fan / siren -> toggle /// button / input_button -> press /// scene / script -> turn_on (activate / run) +/// automation -> trigger /// /// Unknown/read-only domains (binary_sensor, sensor, a newer HA domain) return /// null: no guessed actuation, the badge stays read-only. @@ -201,6 +212,8 @@ HaControlAction? haPrimaryAction(String domain) { return _kSceneActions.first; case 'script': return _kScriptActions.first; + case 'automation': + return _kAutomationActions.first; default: return null; } diff --git a/apps/ios/Crumb/Features/HomeAssistant/HomeAssistant.swift b/apps/ios/Crumb/Features/HomeAssistant/HomeAssistant.swift index 10de1f4a..f03af373 100644 --- a/apps/ios/Crumb/Features/HomeAssistant/HomeAssistant.swift +++ b/apps/ios/Crumb/Features/HomeAssistant/HomeAssistant.swift @@ -394,6 +394,8 @@ extension HA { return [HAAction("turn_on", "Activate", "film")] case "script": return [HAAction("turn_on", "Run", "play.fill")] + case "automation": + return [HAAction("trigger", "Trigger", "bolt.fill")] default: return [] } @@ -448,6 +450,7 @@ extension HA { case "light", "switch", "fan", "siren": return "toggle" case "button", "input_button": return "press" case "scene", "script": return "turn_on" + case "automation": return "trigger" default: return nil } } diff --git a/docs-site/docs/integrations/home-assistant.md b/docs-site/docs/integrations/home-assistant.md index dc08d2d8..3ac28d0f 100644 --- a/docs-site/docs/integrations/home-assistant.md +++ b/docs-site/docs/integrations/home-assistant.md @@ -21,8 +21,9 @@ What works today: - **Live entity badges pinned on the video**, with an icon, shape, color, size, and opacity you choose per entity. - **Control from the badge.** Entities linked with the **Control** role, lights, - switches, fans, sirens, covers, locks, buttons, scenes, and scripts, can be - operated right from the badge: tap a simple device to fire its action, or open + switches, fans, sirens, covers, locks, buttons, scenes, scripts, and + automations, can be operated right from the badge: tap a simple device to fire + its action (an automation triggers, a scene activates, a script runs), or open a small action card for a device like a cover or lock that has more than one meaningful action. - **A Home Assistant hub** in the console: connection settings, every linked @@ -93,8 +94,8 @@ picker scoped to the domains that role accepts: detail card can show, for example, `72°F` instead of a bare number. - The **Control** picker searches every domain Crumb can actuate: `light`, `switch`, `fan`, `siren`, `cover`, `lock`, `button`, `input_button`, `scene`, - and `script`, grouped by domain. If you are looking for a switch or a light, - this is the button; they are not in the sensor pickers. + `script`, and `automation`, grouped by domain. If you are looking for a switch + or a light, this is the button; they are not in the sensor pickers. Every picker has a search box, and an entity already linked under that role shows as "(linked)" instead of being hidden. Nothing is saved until you hit diff --git a/services/api/src/ha.rs b/services/api/src/ha.rs index 11b3b50a..e89f16cd 100644 --- a/services/api/src/ha.rs +++ b/services/api/src/ha.rs @@ -126,6 +126,7 @@ const HA_ACTION_ALLOWLIST: &[(&str, &[HaActionSpec])] = &[ ("input_button", &[spec("press")]), ("scene", &[spec("turn_on")]), ("script", &[spec("turn_on")]), + ("automation", &[spec("trigger")]), ]; /// The kind of numeric value an action carries. Kept as an enum (not a bare @@ -410,7 +411,8 @@ struct EntitiesQuery { /// A single HA domain (e.g. `binary_sensor`, `sensor`, `light`, `cover`), or /// one of the role aliases: `controls` (every actuatable domain from /// `HA_ACTION_ALLOWLIST`: light, switch, fan, siren, cover, lock, button, - /// `input_button`, scene, script) or `sensors` (numeric `sensor`). Omitted ⇒ + /// `input_button`, scene, script, automation) or `sensors` (numeric + /// `sensor`). Omitted ⇒ /// the union of all of the above (motion binary sensors + numeric sensors + /// every controllable domain). domain: Option, @@ -1013,8 +1015,8 @@ async fn get_entities( let s = effective_settings(&state).await?; let domains: Vec<&str> = match q.domain.as_deref() { // Actuator role: every domain the action endpoint can drive (light, - // switch, fan, siren, cover, lock, button, input_button, scene, script), - // derived from the allowlist so the two can never diverge. + // switch, fan, siren, cover, lock, button, input_button, scene, script, + // automation), derived from the allowlist so the two can never diverge. Some("controls") => control_domains(), // Numeric-sensor role (temperature/humidity/... display links). Some("sensors") => vec!["sensor"], @@ -1934,6 +1936,7 @@ mod tests { ("input_button", &["press"]), ("scene", &["turn_on"]), ("script", &["turn_on"]), + ("automation", &["trigger"]), ]; for (domain, actions) in allowed { for action in *actions {