Home Assistant 里最危险的传感器未必显示 unknown。它也可能保留着一个格式正确、看起来合理,却已经过期几个小时的数值。
外部 API 自动化因此需要两类输入:业务值回答价格、温度或空气质量是多少;质量状态回答这个值能否用于当前决策。把两者压在一个实体状态里,系统只能在正常路径工作。断网、限流、缓存、重启和恢复都会变成隐蔽分支。
最小解法是一份三态合同:valid、unknown 和 stale。
三态分别允许什么
| 质量状态 | 定义 | 展示行为 | 控制行为 |
|---|---|---|---|
valid |
原始实体可用,值通过格式与范围检查,数据年龄小于 TTL | 显示当前值 | 允许执行对应动作 |
unknown |
当前没有可信观测 | 可以显示最后有效值,但必须标注原因 | 阻断高影响动作,超过宽限期后告警 |
stale |
数值仍存在,但最后可信上报已超过 TTL | 显示最后有效值和年龄 | 停止控制或进入有界降级 |
Home Assistant 原生还区分 unknown 与 unavailable。官方 Sensor 文档 的定义是:unknown 表示状态尚未知,unavailable 表示实体当前不可用。三态合同可以把两者映射到决策层的 unknown,同时用 reason 属性保留原始原因。实体缺失、格式错误和数值越界也可以进入同一质量态,但原因应各自记录。
这样做的价值是让下游自动化只消费稳定接口,不必在每条规则里复制一串异常字符串。
只过滤 unknown 会留下另一半故障
社区里常见的修复是给触发器增加过滤条件:
not_from:
- unknown
- unavailable
not_to:
- unknown
- unavailable
它能减少重启或短暂断线造成的误触发,却没有解决恢复。
Home Assistant 当前的 Automation triggers 文档 明确说明,多数以实体为目标的触发器在实体从 unknown 或 unavailable 恢复时不会触发。假设电价 API 失效期间错过一次更新,恢复时价格已经低于阈值,原有自动化可能因为没有新的跨阈值过程而继续沉默。
这里有两个独立目标:
- 异常期间不能误执行。
- 恢复后要重新判断当前规则是否仍应执行。
只做第一项,相当于把误触发改成漏触发。可靠自动化必须同时定义故障入口和恢复出口。
把一个传感器拆成四个对象
建议把每个外部 API 数据源拆成四层:
- 原始实体:由集成维护,例如
sensor.vendor_price。 - 最后有效值:只在原始值通过检查时更新,供展示和诊断使用。
- 质量实体:集中输出
valid、unknown或stale,并附带原因、数据年龄和 TTL。 - 业务自动化:只有质量状态允许时,才使用原始值执行动作。
这能阻止一个常见错误:为了让仪表盘好看而保留末值,结果控制自动化也把旧值当成当前事实。展示需要连续性,控制需要证据新鲜度,两者可以读取同一个末值,但权限规则不同。
Home Assistant 官方 Template integration 已经提供所需原语,包括 availability、has_value、数值检查,以及用 trigger-based template sensor 保留最后有效数值的示例。
下面是一个最小末值实体:
template:
- triggers:
- trigger: state
entity_id: sensor.vendor_price
not_to:
- unknown
- unavailable
conditions:
- condition: template
value_template: "{{ is_number(states('sensor.vendor_price')) }}"
sensor:
- name: "Vendor Price Last Valid"
unique_id: vendor_price_last_valid
state: "{{ states('sensor.vendor_price') }}"
unit_of_measurement: "EUR/kWh"
attributes:
value_changed_at: "{{ now().isoformat() }}"
value_changed_at 只说明派生实体何时捕获了变化后的合法值。它不是外部 API 成功返回的时间,更不能直接当作数据新鲜度证明。
先弄清时间戳证明了什么
Home Assistant 的 State 对象有三个容易混淆的时间:
last_changed:主状态值最后一次变化。last_updated:主状态或属性最后一次变化。last_reported:集成最后一次向 Home Assistant 写入状态,包括数值没有变化的重复写入。
当前 Home Assistant Core 源码 对重复状态写入也会生成 state-reported 事件。因此,当某个集成在每次成功轮询后都写入状态时,last_reported 比前两个时间更接近接收心跳。
它依然只证明 Home Assistant 收到了一次写入。若集成持续写入上游缓存,数据业务时间仍可能过期。新鲜度信号应按以下顺序选择:
- 外部服务返回的观测时间。
- 集成记录的成功请求时间或心跳。
- 已验证集成会在每次成功轮询后写入时,使用
last_reported。 - 只有状态或属性变化确实等价于成功取数时,才用
last_updated。 - 只有业务值本应在 TTL 内变化时,才用
last_changed。
这个区别对稳定数值很重要。水箱液位、室温和电价可能连续多次完全相同。官方 State trigger 文档 给出的 to: null 加 for 示例,检测的是状态多久没有变化,不能证明数据多久没有送达。
建立一个集中质量实体
下面的例子假设原始实体稳定存在,并且已经验证该集成的 last_reported 可代表成功上报。TTL 设为 10 分钟:
template:
- sensor:
- name: "Vendor Price Quality"
unique_id: vendor_price_quality
state: >
{% set raw = states('sensor.vendor_price') %}
{% if raw in ['unknown', 'unavailable'] %}
unknown
{% elif not is_number(raw) %}
unknown
{% elif (now() - states.sensor.vendor_price.last_reported).total_seconds() > 600 %}
stale
{% else %}
valid
{% endif %}
attributes:
reason: >
{% set raw = states('sensor.vendor_price') %}
{% if raw in ['unknown', 'unavailable'] %}
{{ raw }}
{% elif not is_number(raw) %}
parse_error
{% elif (now() - states.sensor.vendor_price.last_reported).total_seconds() > 600 %}
ttl_expired
{% else %}
ok
{% endif %}
source_age_seconds: >
{{ (now() - states.sensor.vendor_price.last_reported).total_seconds() | int }}
ttl_seconds: 600
last_valid_value: "{{ states('sensor.vendor_price_last_valid') }}"
这里使用 now() 是为了让数值保持不变的实体也能随时间进入 stale。官方 日期与时间模板文档 说明,包含 now() 或 utcnow() 的模板每分钟重新计算一次。
投入实际使用前还要按数据源调整:
- 外部 API 有业务时间戳时,优先使用它。
- 对数值增加合理范围或 Schema 校验。
- TTL 按动作截止时间确定。电价、降雨、门磁和空气质量的有效期不同。
- 集成可能被删除时,补上实体缺失分支。
- 一分钟分辨率不合适时,改用 time pattern 驱动的 trigger-based template。
集中质量实体的目的不是再造一层复杂框架,而是删除散落在每条自动化里的重复判断。
把恢复定义成正式触发器
业务自动化需要同时监听值变化和质量恢复:
automation:
- alias: "Run flexible load when price is valid and low"
mode: single
triggers:
- trigger: state
entity_id: sensor.vendor_price
to: null
id: value_changed
- trigger: state
entity_id: sensor.vendor_price_quality
to: valid
id: recovered
conditions:
- condition: state
entity_id: sensor.vendor_price_quality
state: valid
- condition: numeric_state
entity_id: sensor.vendor_price
below: 0.20
actions:
- action: script.turn_on
target:
entity_id: script.set_flexible_load_on
被调用的脚本应当具备幂等性。重复执行两次,最终设备状态应相同,也不能制造两条通知或两次不可逆副作用。mode: single 可以避免重叠运行,但不能保证两个先后到达的触发器永远只执行一次。
高影响动作还应增加三项控制:
- 稳定窗口:连续收到若干成功观测后再恢复为
valid。 - 补做时限:只在动作仍有业务价值时补做。刚恢复的电价信号可能仍有效,几小时前的开门事件通常不该重放。
- 决策键:记录已经处理的观测 ID、时间窗口或策略版本,防止恢复路径重复执行。
恢复由此成为可测试的业务事件,而不是碰巧发生的一次状态变化。
每类动作需要自己的失败策略
| 动作类型 | valid |
unknown |
stale |
恢复后 |
|---|---|---|---|---|
| 仪表盘 | 当前值 | 末值加原因 | 末值加年龄 | 稳定后移除提示 |
| 通知 | 执行正常规则 | 宽限期后告警 | TTL 到期告警并抑制重复 | 只发一次恢复通知 |
| 可逆舒适性控制 | 允许 | 保持安全状态 | 进入有界降级 | 重新计算当前策略 |
| 安防或安全控制 | 全部检查通过后允许 | 失败关闭 | 失败关闭 | 要求新证据,必要时人工确认 |
| 历史记录 | 记录值与质量 | 记录失败原因 | 记录年龄与 TTL | 关联同一故障事件 |
除非零在业务上确实安全且语义正确,否则不要用零替代未知输入。给温度、价格或运动状态填默认值,可能在隐藏故障的同时授权错误动作。
去抖与恢复迟滞要分开
外部 API 偶发失败很常见。一次失败就报警会制造噪声,无限等待又会隐藏故障。进入与退出质量态应采用不同条件:
- 连续失败若干次或超过短宽限期后进入
unknown。 - 到达业务 TTL 时进入
stale。 - 按动作影响程度,收到一次或多次成功观测后恢复
valid。 - 故障告警只发一次,持续期间更新时长,恢复后再发一次通知。
- 业务阈值的迟滞和数据质量的迟滞分别配置。两者解决的是不同问题。
使用 for 实现宽限期时要记住,Home Assistant 重启或自动化重载会清空这类计时。必须跨重启保存的截止时间,应放进 input_datetime 等 Helper。这是官方模板触发器文档给出的持久化方案。
用故障注入完成验收
模板在 Developer Tools 中能渲染,只证明语法通过。上线前至少跑完以下固定任务集:
| 测试 | 注入条件 | 预期结果 |
|---|---|---|
| 正常更新 | 新鲜合法值依次越过业务阈值 | 质量为 valid,动作符合规则 |
| 原生 unknown | 原始实体进入 unknown |
质量为 unknown,动作阻断,原因保留 |
| 原生 unavailable | 集成或实体不可用 | 不使用默认值执行 |
| 数值冻结 | 保留末值并停止上报 | TTL 到期时进入 stale |
| 恢复 | 恢复新鲜合法值 | 只产生一次恢复事件并重算当前规则 |
| 反复抖动 | 快速交替成功与失败 | 不产生动作风暴和告警风暴 |
| 重启 | 在宽限期、过期等待和恢复阶段分别重启 | 持久截止时间正确,或按文档明确重置 |
| 实体缺失 | 删除或重命名数据源 | 失败关闭并输出可诊断原因 |
验收时检查 Automation Trace 和 History,统计业务动作次数、通知次数、质量转换和恢复时间。最终仪表盘看起来正常,不能替代状态路径正确。
从最小合同开始
先选一个真实 API,只实现一个质量实体、一个 TTL、一个幂等动作、一条告警和上述测试表。等运行记录出现高频失败后,再增加集成专属时间戳、连续失败计数、持久截止时间或全屋汇总。
这种顺序能保持系统轻量,也让每次人工接管都有明确分类:缺少状态、缺少转换、TTL 错误、重复动作或降级策略不合适。真实失败记录会告诉你下一步该补哪层控制。
相关架构可继续参考 本地视觉、云端推理 和 AI Agent 规则为什么需要提交门控。
常见问题
unknown 和 unavailable 有什么区别?
unknown 表示状态尚未知,unavailable 表示实体当前不可用。决策质量层可以把两者映射到 unknown,同时保留原始原因供诊断与恢复策略使用。
是否应该忽略从 unknown 或 unavailable 恢复的变化?
过滤这些变化可以减少误触发,但也可能漏掉恢复后的有效决策。业务动作应阻断无效输入,同时用独立恢复触发器重新判断当前规则。
怎样判断传感器已经过期?
用真正代表成功取数的时间与当前时间比较。优先使用上游观测时间;只有确认集成每次成功轮询都会写入时,才使用 last_reported。数值长期不变并不等于数据没有上报。
可以一直保留最后有效值吗?
可以用于展示、诊断和有限降级。控制自动化还必须读取独立质量态,高影响动作只在 valid 时执行。
state trigger 的 for 能跨重启吗?
不能。官方 State trigger 文档说明,Home Assistant 重启或自动化重载会重置计时。需要跨重启的截止时间应写入 Helper。
availability 已经足够了吗?
availability 能避免派生实体把非法输入展示为正常数值。数据过期、恢复补做、动作权限、告警去重和持久截止时间仍需单独定义。
参考资料
- Home Assistant,Sensor states
- Home Assistant,Automation triggers
- Home Assistant,State trigger
- Home Assistant,Template integration
- Home Assistant,Working with dates and times
- Home Assistant Core,State implementation
- Home Assistant Community,Let us see when last sensor data was received
- Home Assistant Community,Handling stale sensors that drive thermostats