Administrator
Published on 2026-09-15 / 11 Visits
0
0

Home Assistant 外部 API 自动化:有效、未知与过期三态合同

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 失效期间错过一次更新,恢复时价格已经低于阈值,原有自动化可能因为没有新的跨阈值过程而继续沉默。

这里有两个独立目标:

  1. 异常期间不能误执行。
  2. 恢复后要重新判断当前规则是否仍应执行。

只做第一项,相当于把误触发改成漏触发。可靠自动化必须同时定义故障入口和恢复出口。

把一个传感器拆成四个对象

建议把每个外部 API 数据源拆成四层:

  1. 原始实体:由集成维护,例如 sensor.vendor_price。
  2. 最后有效值:只在原始值通过检查时更新,供展示和诊断使用。
  3. 质量实体:集中输出 valid、unknown 或 stale,并附带原因、数据年龄和 TTL。
  4. 业务自动化:只有质量状态允许时,才使用原始值执行动作。

这能阻止一个常见错误:为了让仪表盘好看而保留末值,结果控制自动化也把旧值当成当前事实。展示需要连续性,控制需要证据新鲜度,两者可以读取同一个末值,但权限规则不同。

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 收到了一次写入。若集成持续写入上游缓存,数据业务时间仍可能过期。新鲜度信号应按以下顺序选择:

  1. 外部服务返回的观测时间。
  2. 集成记录的成功请求时间或心跳。
  3. 已验证集成会在每次成功轮询后写入时,使用 last_reported。
  4. 只有状态或属性变化确实等价于成功取数时,才用 last_updated。
  5. 只有业务值本应在 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 能避免派生实体把非法输入展示为正常数值。数据过期、恢复补做、动作权限、告警去重和持久截止时间仍需单独定义。

参考资料


Comment