动作:服务调用、延迟与并行

动作是自动化的手

触发和条件解决「想不想做」,动作解决「做什么、怎么做」。一条自动化的动作部分本质是一串有序指令:调用服务、等待、分支、循环、再调用服务。本篇把 action 段里能用的结构讲清楚,并给出排布动作顺序的几条原则。

调用服务:最核心的一个动作

绝大多数动作最终都落在「调用服务」上,也就是配置里的 service: 域.动作。服务分为两类:

服务类别 例子 特点
设备控制类 light.turn_on、switch.toggle 面向实体,立即生效
系统类 notify.notify、automation.reload 面向功能,发通知或改配置

调用服务时三样东西要写清:服务名、目标实体、数据。以开灯为例,light.turn_on 不带数据只是「点亮」,配上数据就是「以 60% 亮度、4000K 色温、2 秒过渡点亮」,体验完全不同。新手上路最快的练习方式,是在「开发者工具 → 服务」页签里挨个试:选服务、填目标、点「调用」,满意了再把同样的内容粘进自动化。

一个常见疑问是 turn_on 和 toggle 的区别:前者明确「打开」,后者是「切换当前状态」。自动化里建议一律用 turn_on/turn_off,语义明确;toggle 在「不确定当前状态」时才用,但那往往意味着你的自动化缺了状态判断。

通知是另一个高频动作。notify.notify 接收 message,还能附 title、data(按平台不同可带图片、声音、点击行为)。做通知类自动化时先想清楚两件事:给谁看、点开之后干什么。只发消息、点开却没动作的通知,价值连一条便利贴都不如。所以成熟的做法通常是通知带上「动作按钮」或在消息里附实体当前状态,让你一眼知道要不要处理。

还有一个细节:服务调用默认会等待返回。对于某些慢响应设备,如果不想阻塞流水线,可以研究集成文档里是否提供非阻塞调用;通常空调、灯具这类都足够快,不必过度担心。

等待的艺术:delay 与 wait_template

delay 就是延迟若干时间,用于给设备留出动作时间。比如「开投影仪 → 延迟 10 秒 → 切换到 HDMI 输入」,因为设备开机需要预热。delay 的缺点是「死等」:无论设备是否就绪,它都占着自动化不走。

更聪明的等待是 wait_template:它每隔几秒评估一次模板,条件成立立即继续,还能配 timeout 设定最长等待时间,避免卡死。同样是投屏场景,写「等到投影仪状态为 on 再继续」,快则 5 秒慢则 30 秒都能自适应。

action:
  - service: media_player.turn_on
    target:
      entity_id: media_player.projector
  - wait_template: >-
      {{ is_state('media_player.projector', 'on') }}
    timeout: "00:00:30"
  - service: media_player.select_source
    target:
      entity_id: media_player.projector
    data:
      source: HDMI

delay 与 wait_template

分支与循环:choose、if、repeat

choose 是 YAML 里的 if/else:一列 conditions 搭一列 sequence,外加可选的 default。适合「同一触发下按情况执行不同动作」,比如门铃响时根据「是否在开会」决定是响喇叭还是只推通知。

repeat 负责循环,三种模式:count 固定次数、while 条件成立就继续、until 直到条件成立。典型用途是「每 5 分钟检查一次晾衣机状态,直到晾干为止」,轮询类需求不要靠多条自动化堆,一个 repeat 搞定。repeat 内部必须放一个 delay 或 wait_template,否则就是忙循环,会把 CPU 吃满。

给一个 choose 的具体写法,直观感受一下结构:

action:
  - choose:
      - conditions:
          - condition: state
            entity_id: input_boolean.meeting_mode
            state: "on"
        sequence:
          - service: notify.notify
            data:
              message: 门铃响了,你在开会,只推送不响铃
      - conditions:
          - condition: state
            entity_id: input_boolean.meeting_mode
            state: "off"
        sequence:
          - service: media_player.play_media
            target:
              entity_id: media_player.speaker
            data:
              media_content_id: doorbell.mp3
    default:
      - service: notify.notify
        data:
          message: 门铃响了

两套动作共用一个触发(门铃事件),靠 choose 路由——这就是上一章「动作内做分支」的落地形态。

并行与封装:script 与场景

自动化内的 action 默认是顺序执行的。想要并行有两条路:

  1. parallel 模式:让这条自动化支持并行实例,但不同实例操作同一台设备时可能互相打架。
  2. script 的 parallel:把一段动作序列封装成脚本,脚本有自己的 mode 参数。

后者更可控。一个「再见模式」脚本里可以同时下发关灯、关空调、关窗帘,方式是在脚本内用 parallel 结构,或干脆把互不依赖的步骤拆成两个脚本由动作同时调用。脚本与场景的完整分工在「场景与脚本的分工」一篇展开,这里只要记住:自动化越来越长的时候,就是该抽成脚本的时候。

变量:在动作里传递上下文

从触发到动作一路携带的信息叫「触发变量」,常用如下:

  • trigger.platform:是哪个触发平台点的火。
  • trigger.entity_id:状态类触发的实体。
  • trigger.event.data:事件类触发的数据包。
  • trigger.to_state.state:状态机里变化后的新值。

配合 action里的 variables 段,可以先把中间值算好再逐步使用,可读性比深层嵌套模板好得多。一个典型场景:IFTTT webhook 带参数进来,自动化里先用变量把参数取出来,再分发给不同设备,后续维护时一目了然。

变量段的写法:

action:
  - variables:
      who: "{{ trigger.event.data.who }}"
      room: "{{ trigger.event.data.room }}"
  - service: light.turn_on
    target:
      entity_id: "light.{{ room }}"

注意模板里的引号:变量值渲染出来是字符串,拼进实体 ID 时要保证两侧的点和引号都对得上。这种「触发参数 → 变量 → 实体」的链路,是做通用自动化(一条走天下)的核心技巧,也是从「写死配置」走向「数据驱动」的第一步。

排布动作的四条原则

  1. 慢设备先启动,快设备后启动:投影仪比灯慢,就先开投影仪。
  2. 先分支后执行:靠前的 choose 决定路径,避免重复动作。
  3. 所有等待都加 timeout:防止自动化永远卡在某一步。
  4. 长动作抽脚本:超过十行的 action,拆出去对你和未来的自己都好。

动作编排四原则

延伸阅读

  • 脚本与场景的分工详见本系列「场景与脚本的分工」一篇。
  • 服务调用的完整清单在「开发者工具 → 服务」里可查,也可参考本系列「自动化的调试与日志追踪」。
  • 长期运行 HA 的硬件方案可对比 《软路由指南》 与 《NAS 搭建指南》。