状态持久化与开机还原

重启之后,世界变了没有

HA 的实体状态分两类住处:一类在内存里(传感器、设备实时状态),一类落了盘(helper 实体)。重启 HA 之后,传感器会重新上报、设备会重新连接,但自动化运行到哪一步、某件事做没做过,这类「过程记忆」默认是不保存的。

举个真实的痛:你有一条「电价谷段给电动车充电」的自动化,中途 HA 因为更新重启了。重启后自动化不知道自己充过没有,可能重复启动、也可能彻底漏掉——除非你主动把「做到哪一步」写进一个持久的地方。这就是状态持久化要解决的问题。

类似地还有「窗帘开到一半 HA 重启了」「洗衣机还剩 20 分钟提醒被中断了」这些场景,它们都指向同一个设计缺陷:自动化把进度放在了内存里这个「不保险的口袋」。

哪些状态会丢,哪些不会

状态类型 存储位置 重启后
传感器实时值 内存 + 集成重连 恢复(依赖设备上报)
helper 实体 数据库(recorder) 恢复且立即可用
自动化运行状态 内存 丢失,从头开始
脚本内的变量 内存 丢失
仪表盘临时状态 前端 丢失

结论很清楚:任何需要跨重启记住的东西,都必须放进一个实体里——helper 是最方便的载体,其次是自定义传感器或 MQTT 的 retain 消息。

一个反直觉的细节:自动化本身的「开/关」状态是持久化的。你在界面里禁用了一条自动化,重启后它还是禁用——但这只记「启用状态」,不记「执行到哪」。所以别误以为自动化开着就等于它记得自己的进度,两者是两码事。

为什么不能用「延迟」记状态

新手常犯的一个错误是用超长 delay 当记忆:「浇水自动化延迟 10 小时再关阀门」。看起来省事,实则脆弱:HA 重启会杀掉所有运行中的自动化,delay 直接归零;而且 delay 期间这条自动化处于「占用」状态,mode 配不对就再也点不着它。延迟只用来等设备,不用来跨时间——跨时间的活儿永远交给 helper 或定时触发。这是本篇最该带走的一条原则。

用 helper 记录「做到哪一步」

最常见的持久化模式是「步骤计数器」:一个 input_number 记进度,自动化每完成一步就加一。配上 input_boolean 记「整个任务是否完成」,就有了一套最小的状态机。

一个「浇花循环自动化」的结构可以是:

trigger:
  - platform: time
    at: "07:00:00"
condition:
  - condition: state
    entity_id: input_boolean.watering_done
    state: "off"
action:
  - service: switch.turn_on
    target:
      entity_id: switch.valve
  - delay: "00:10:00"
  - service: switch.turn_off
    target:
      entity_id: switch.valve
  - service: input_boolean.turn_on
    target:
      entity_id: input_boolean.watering_done

每天到点先问「今天浇过吗」,没浇就浇并标记。哪怕中途重启,标记还在,不会重复。次日零点再用一条自动化把标记复位。这套「问一句再做、做完留痕」的模式,是所有定时任务都应该长成的样子。注意标记里最好同时存日期,跨天判断才不会错。

这套模式还能扩展成多步骤状态机:input_number 记「当前第几步」,每步完成后加一,开机还原时按步号决定从哪继续。比如「洗衣机的自定义流程」「充电桩的分阶段充电」,道理都一样:把进程变成数据。数据落在 helper 里,重启就成了无关紧要的小事。

顺带说一句,这套做法也方便调试:自动化出问题时,打开 helper 实体看一眼步号和标记,卡在哪一步一目了然,比翻日志一行行找强。

开机还原:homeassistant_start 事件

homeassistant_start 是事件总线上的一个特殊事件:每次 HA 完成启动时触发。它是「开机还原」的天然锚点。典型用法:

trigger:
  - platform: event
    event_type: homeassistant_start
action:
  - service: script.restore_state

开机还原通常要做三类事:

  1. 恢复模式类开关:如果上次是「度假模式」,重启后该开还得开——落了盘的 helper 本来就在,无需处理;但某些「从 MQTT 或云端取」的标志位需要主动重设。
  2. 触发被错过的定时任务:重启发生在早上 7 点 01 分,而浇水定在 7 点整,那条 time 触发就错过了。开机时检查「今天该做的事做了没」,没做就补。判断「今天」时用 now().day 对比 helper 里存的日期,能跨午夜也不含糊。
  3. 校准已知状态:比如重启后把窗帘、空调主动设回上次记录的状态,避免设备自启。这步通常配合 store 在 helper 里的「上次目标值」一起做。

开机还原也有要避开的操作:别在开机事件里一股脑调用大量服务。所有设备刚重连,部分实体可能还没就绪,立刻下发命令容易吃「实体不存在」的错。先 wait_template 等关键实体上线,再执行还原动作,急不得。另一个替代方案是监听 entity_registry 相关的集成就绪事件,或者干脆把还原逻辑延后 60 秒执行,土但有效。

开机还原流程

recorder 与历史数据

说到持久化绕不开 recorder 集成。它把状态变化写进数据库,决定了你能看多久的历史曲线。默认配置下它用内置 SQLite,数据量大后查询会变慢。几条调优经验:

  • 用 exclude 排掉不关心的实体(比如各种内部传感器),数据库立瘦一半。
  • purge_keep_days 按需设长或缩短,家里传感器多就别土豪地默认 10 天。
  • 数据量真的大时换 PostgreSQL 或 MariaDB,部署方式可参考 《NAS 搭建指南》 里的数据库容器章节。

还有一个绕不开的现实:数据库需要备份。HA 的快照功能会带走 SQLite 文件,但换了外部数据库就要自己操心,把它纳入 NAS 的定期备份最省事。具体方案可参考 《群晖 DSM 指南》 的备份章节。

一个判断标准:仪表盘历史卡片转圈超过两秒,就是该调 recorder 的信号了。

再提醒一个常见误区:历史数据≠状态持久化。recorder 记的是「过去发生了什么」供你查看图表,自动化运行时读取「当前状态」并不依赖它。把 recorder 当持久化用(比如用 SQL 查历史做判断)不仅慢,而且概念上就是错的——持久化要的是「活的状态」,不是「死的历史」。

MQTT retain:另一条持久化通道

如果家里有 MQTT 代理,可以用 retain 消息做轻量持久化:发布带 retain 标志的消息后,代理会记住最后一条,订阅者(包括重启后的 HA)一上线就能收到。适合「当前的目标温度」「上次的模式名称」这类单个值。

retain 的好处是不占 HA 自己的数据库,坏处是布局在 HA 之外,换设备或重置代理时要手动清理残留消息。

两类持久化载体

选型建议很简单:只在 HA 内部用,选 helper;已经有 MQTT 基础设施且想跨多个系统共享状态,才选 retain。多数家庭场景,helper 就够了。

持久化设计的检查清单

把本篇压缩成四个自问自答,写每条自动化时过一遍:

  1. 这条自动化要记住什么? 进度、标记、上次的值。
  2. 记到哪里? helper 实体最省心。
  3. 开机时要不要补做? 配 homeassistant_start 检查标记。
  4. 异常时有没有痕迹? 关键步骤推一条带时间的通知或日志。

状态持久化设计四步

这四步看起来繁琐,做惯了就是肌肉记忆。当你某天升级 HA 重启后,发现家里所有自动化都若无其事地继续运转,那种踏实感值得一开始的这点麻烦。

延伸阅读

  • 事件总线的使用见本系列「事件总线与自定义事件」一篇。
  • 调试重启后行为可参考「自动化的调试与日志追踪」。
  • 数据库部署与备份可参考 《群晖 DSM 指南》,网络层稳定见 《软路由指南》。