事件总线与自定义事件

HA 的「神经系统」

前几章我们一直在和实体状态打交道:灯是开是关、温度是多少。但 HA 内部还有一条更基础的信息通道:事件总线(event bus)。几乎所有重要动作——状态变化、服务被调用、集成加载、定时器触发——都会先在事件总线上广播一次,再由各处消化。

为什么这套机制对你重要?因为它允许自动化监听「任意事件」而不只是「实体状态」。当一个设备没有实体、只有一个动作信号(比如无线按钮按下)时,事件总线是你唯一能抓住它的地方。理解事件总线,是从「写配置」走向「做集成」的分水岭。

还有一个现实好处:总线上的一切都是「公开」的。任何自动化都能听到任何事件,不需要互相调用或引用。这给你一种类似「广播台」的架构能力——发一条消息,关心的人自己听,不用维护谁调用谁的网状关系。

事件长什么样

一个事件由三部分组成:event_type(类型名)、event_data(数据载荷)、time_fired(触发时间)。最常见的三类事件:

还有第三种:集成自己发的事件,如 zha_event、deconz_event、mqtt 集成的消息到达。这类事件最容易迷惑新手:它们不在实体列表里出现,只能去监听页才能看到全貌。第一步永远是打开监听页抓真实数据包,看清楚有哪些字段,再写过滤条件。

事件类型 何时发出 典型用途
state_changed 任何实体状态变化 总线 activity 的来源
call_service 任何服务被调用 审计、回调、联动
homeassistant_start 系统启动完成 开机还原锚点
集成事件 设备动作发生 按键、报警等信号

state_changed 是流量最大的事件——家里几个传感器每秒都在刷新它。这也是为什么监听总线要谨慎:逮着 state_changed 不做过滤地监听,等于把每条自动化都挂在一个高流量管道上。

state_changed 的数据包里装着三样东西:实体 ID、旧状态(old_state)、新状态(new_state)。如果你想写一条「只要任何温度传感器变化就记录一行日志」的自动化,在事件数据里按实体 ID 前缀过滤即可,比写几十条状态触发干净得多。

call_service 则是「审计员」:任何自动化调用 light.turn_off 都会发一条,数据里带着服务名、目标实体和传入参数。高级玩家用它做「谁关了我的灯」式的反向追踪——所有动作都有迹可循,排查「灯怎么自己灭了」不再靠猜。

事件命名小约定

自定义事件最怕的不是写不出来,而是半年后看不懂。三条建议:名字用「名词 动词」结构(如 person_home、door_bell_pressed);数据字段固定且见名知义(who、which、value);必要时在配置注释里写一句「这个事件谁发谁收」。约定越早定,后期自动化越多越受益。

监听事件:listen_event 的三个位置

写自动化时,platform: event 就是最直接的「监听」。能监听的还有两个进阶工具:AppDaemon(用 Python 注册回调)与 Node-RED(用事件节点接线)。三者本质上都是「告诉系统:这个事件来了就喊我」。

一条典型的自动化监听:

trigger:
  - platform: event
    event_type: zha_event
    event_data:
      device_ieee: 00:0d:6f:...
action:
  - service: light.toggle
    target:
      entity_id: light.corner

设备事件(如 Zigbee 按钮的单击、双击)是事件总线最有价值的常客:它们没有「状态」可观察,只有「事情发生了」这个信号。这类设备做不了状态触发,只能做事件触发——这是触发器选择上的一条硬约束。

设备的「按键事件」通常还带一个 command 字段,区分单击、双击、长按。监听时把它写进 event_data 过滤条件,一个按钮就能分发多种动作:单击开灯、双击关全屋、长按开启观影模式。无线按钮便宜省电,是做低成本场景开关的首选。

fire_event:发出自己的事件

事件总线是双向的:你也能让它响。自定义事件的典型用途是跨自动化通信:一条自动化「宣告发生了某件事」,另外几条「订阅这件事」,彼此互不依赖。

举例:检测到「有人到家」这件事后,不直接控制任何设备,而是发一个自定义事件:

action:
  - event: person_home
    event_data:
      who: alice

然后灯光、空调、音乐三条自动化各自监听 person_home,按自己的逻辑响应(也可忽略)。发布者和订阅者解耦——新增一条响应自动化完全不用碰原代码,这在自动化多起来之后是救命的优雅。

这套「扇出」架构还有个隐藏优势:替换检测逻辑不费劲。哪天你把「回家检测」从手机定位换成 mmWave 存在传感器,只要新方案依然发出同一个事件,下游自动化一行都不用改。接口稳定,实现可换,这就是解耦的回报。

自定义事件的扇出

事件做「一次总线」的适用边界

事件虽好,但不是所有通信都该走事件。判断标准:

  1. 瞬间发生的事 → 事件:门铃、按钮按下、到家、警报触发。
  2. 持续存在的状态 → 实体:温度、灯的亮度、模式开关。
  3. 需要历史和图表 → 实体:事件不进历史曲线,传感器才进。
  4. 需要被多个自动化共享判断 → 事件扇出或 helper:两者皆可,看是否需要记忆。

把状态错写成事件,你会发现自己需要「记得上次的事有没有发生过」——那是上一章讲的持久化问题。反过来把事件错写成状态(比如让某传感器值从 0 跳到 1 表示「门铃响了」),则要在条件里写各种奇怪判断。先分清「发生了」和「是处在」,工具自然选对。

再给一个具体决策示例:「门铃响了」是事件,「门开着」是状态;「烘干机结束」是事件(多数机器只推一次信号),「室温 26 度」是状态。遇到拿不准的情况,问自己一句话:这件事如果我没接住,还能事后查到吗——查得到的就是状态,查不到的就是事件。

命名小约定之外,还要注意事件是全局可见的:任何集成、任何自动化都能监听你的自定义事件。别用它传递敏感信息(比如明文密码、定位坐标),需要保密的数据应放在实体属性或加密存储里。

调试事件的工具

「开发者工具 → 事件」页面有两个关键功能:

  • 监听事件:填入 event_type 开始监听,然后物理操作设备,总线上流过的事件实时打印在下方。这是写事件触发器前必做的一步:先监听到真实数据,再照着写 event_data 过滤条件,命中率比猜参数高得多。
  • 触发事件:手动 fire 任意事件。开发「订阅端」自动化时,不用每次去按真按钮,手动发事件即可调试,效率天差地别。

调试时还有一招:在事件监听页里先不过滤任何字段,抓到真实数据包后,再决定 event_data 里写哪几个键做过滤。键不是越多越好,只过滤你真正关心的字段,事件数据里多带的字段不会影响匹配。

事件与持久化的边界

事件是「即时的」:发出去的 moment 如果没有订阅者就消失了,HA 不会帮你存。所以凡是「上个月谁回过家」这类回溯需求,不能只靠事件,而要让订阅它的自动化把关键信息写进 helper 或传感器。事件管通信,实体管记忆,分工明确。

打个比方:事件是「喊一嗓子」,实体是「写在黑板上的字」。喊一嗓子快,但没听到就没了;写在黑板上慢一点,但谁路过都能看。自动化架构的成熟度,往往就体现在你多自觉地用黑板而非嗓子。

一套典型的「喊一嗓子 写黑板」联动:监听事件的自动化在动作末尾加一步,把事件的关键字段写进 input_text 或 input_datetime。这样一来下游既能实时响应(事件),也能事后查证(实体),两种需求都照顾到。

如果接住事件后要做的事比较重(比如调一堆设备),记得给订阅自动化选对 mode:事件可能在秒级内连续到达,single 模式会让后面的请求全部丢失,restart 或 queued 才是合理选择。

事件 vs 实体状态

延伸阅读

  • 持久化载体见本系列「状态持久化与开机还原」。
  • Python 方式的事件监听见「AppDaemon 与 Python 脚本」一篇。
  • 消息通道部署可参考 《OpenWrt 入门》 与 《群晖 DSM 指南》。