From de401c4ac76a61111e8341c5de80fbac7b50cee3 Mon Sep 17 00:00:00 2001 From: badbread Date: Sun, 9 Aug 2026 17:20:03 -0700 Subject: [PATCH] feat(ha): trigger Home Assistant automations from the badge Adds the automation domain to the HA control path (the one piece of HA control missing from main). automation.trigger fires the automation's actions immediately, skipping its trigger conditions. - server: ("automation", &[spec("trigger")]) in HA_ACTION_ALLOWLIST + guard test; the picker offers it for free (control_domains derives from the allowlist) - clients: automation -> trigger in each action table (single tap, no confirm, like scene/script): ha_actions.dart, HaModels.kt, HomeAssistant.swift - docs-site HA page: automation listed in the controllable domains Gated by the existing default-off actuators capability; no schema change. Signed-off-by: badbread --- .../src/main/java/video/crumb/app/data/HaModels.kt | 2 ++ .../lib/ui/ha_overlay/ha_actions.dart | 13 +++++++++++++ .../Features/HomeAssistant/HomeAssistant.swift | 3 +++ docs-site/docs/integrations/home-assistant.md | 9 +++++---- services/api/src/ha.rs | 9 ++++++--- 5 files changed, 29 insertions(+), 7 deletions(-) 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 {