Home Assistant Jinja2 模板排障:六类报错决策树与生产模板防错范式
夜里 11 点 40,我刚躺下,手机推送炸了:UndefinedError: 'sensor.bedroom_temperature' is undefined。爬起来看日志,是我下午给卧室温度传感器写的一个 template sensor,实体 ID 少打了一个字母 s——sensor.bedroom_temperatur。模板引擎没报实体不存在,直接报变量 undefined,整个模板渲染失败,那个传感器一直显示 unknown,我却在白天盯着"正常"的预览没发现。
那次之后我把 HA 里的模板报错摸了个遍。HA 里模板报错是比自动化不触发更隐蔽的故障:自动化不触发你还能发现,模板渲染失败经常是静默的——传感器悄悄变成 unknown、自动化条件永远不满足、仪表盘显示 n/a,你根本不知道哪一环断了。更要命的是它不一定是"写错",很可能是某个实体临时离线,今天好好的,明天半夜就炸。这篇文把我线上踩过的坑写全:六类模板报错(undefined 变量、类型错误、过滤器用错、时间与时区、state 属性访问、unavailable 状态)各自怎么认、怎么修,加一张速查决策树和一套防错范式。想看自动化怎么写的先补 自动化基础 和 自动化 YAML 速查库,模板是自动化里最容易出错的那一层。
模板在 HA 里到底在哪几处跑
Jinja2 模板不是只出现在自动化里。理解模板的运行位置,才能知道报错会以什么形态暴露。我习惯把模板运行位分成三类,报错特征完全不同:
| 运行位置 | 报错暴露形态 | 排查入口 |
|---|---|---|
| template sensor / binary_sensor | 实体变 unknown,日志有 TemplateError |
Developer Tools → States + 日志 |
| automation 的 condition / action | 该自动化条件不满足或动作被跳过,日志有错误 | Settings → System → Logs |
| template helper(输入辅助/模板辅助) | 辅助元素直接不可用/显示 n/a | Developer Tools → Template |
三处共用一个引擎,但报错能不能被你看见差别很大。template sensor 渲染失败会静默变 unknown,最坑;automation 里的模板报错只在触发时才暴露;template helper 报错会让整个 helper 失效。所以第一步永远是:先确认报错发生在哪一层,再去查对应位置。
另一个关键点:预览不等于真实运行。Developer Tools → Template 里的预览用的是当前运行态实体,你粘贴进去能预览成功,只说明语法对,不代表保存后真实渲染也成功。实体 unavailable、实体名写错、属性不存在,预览里可能照样给你个值,保存后一跑就炸。
自动化里模板最常出现的三处
自动化里模板不是只在 condition 出现,我有三处高频用法,报错要分着看:
# ① trigger 的 value_template:过滤触发值(比如只在温度变化超过阈值时触发)
trigger:
- platform: state
entity_id: sensor.outdoor_temp
value_template: "{{ (trigger.to_state.state | float(0) - trigger.from_state.state | float(0)) | abs > 2 }}"
# ② condition 的模板条件:不满足时整个自动化静默跳过
condition:
- condition: template
value_template: "{{ states('sensor.living_temp') | float(0) > 26 }}"
# ③ action 的数据模板:把渲染结果塞进服务调用
action:
- service: notify.mobile_app_phone
data:
message: "当前温度 {{ states('sensor.living_temp') | float(0) | round(1) }}°C"
① 报错会让触发失效,② 报错会让条件永远为假,③ 报错会让动作直接失败。三处里 ② 最坑——条件模板报错不抛异常,而是把条件当假处理,自动化就静默不触发,你还以为是逻辑写错。
坑 1:undefined 变量——最经典也最好修
报错:UndefinedError: 'sensor.xxx' is undefined
根因:实体不存在、实体名拼错、或该实体还没被实例化(设备还没连上)。
这是我最常踩的坑。点式访问和函数式访问行为完全不同,这是新手最容易忽略的:
{# ❌ 点式访问:实体不存在时直接抛 undefined,整个模板炸掉 #}
{{ states.sensor.bedroom_temperature.state }}
{# ✅ 函数式访问:实体不存在时返回字符串 'unknown',不抛错,模板继续渲染 #}
{{ states('sensor.bedroom_temperature') }}
{# ✅ 需要默认值:套 default 过滤器,第二个参数 true 表示「undefined 也当作空」 #}
{{ states('sensor.bedroom_temperature') | default('unavailable', true) }}
修复原则:能用 states('sensor.xxx') 就绝不写 states.sensor.xxx.state。前者是 HA 封装好的函数,缺实体时返回 'unknown';后者直接触达 Jinja 上下文,缺了就是 undefined。凡是模板里要用「可能不存在的实体」,一律走函数式 + default()。
我踩得最冤的一次:实体 ID 里有个下划线我漏打了,模板报 'sensor.kitchen_temp_sensor' is undefined,我盯着报错看了十分钟,最后在 Developer Tools → States 里搜索才发现真实 ID 是 sensor.kitchen_temperature_sensor。undefined 报错第一件事永远是去 States 里核对实体 ID 是不是真存在,别急着改模板。
坑 2:类型错误——| float 和 | int 是最常见的雷
报错:TypeError: float() argument must be a string or a number
根因:把不可转换的值丢给了数值过滤器。
这个坑和坑 1 是连体婴:实体 unavailable 时 states() 返回 'unavailable',这是字符串,| float 无法把它转成数字,直接抛 TypeError。我见过太多人写:
{# ❌ 实体 unavailable 时 'unavailable' | float 直接炸 #}
{{ states('sensor.outdoor_temp') | float | round(1) }}
修复:先判断再转换,或者用 float(0) 给个兜底:
{# ✅ 先 is_number 判断,是数字才转换 #}
{{ states('sensor.outdoor_temp') | float(0) | round(1) if is_number(states('sensor.outdoor_temp')) else 'n/a' }}
{# ✅ 或直接用 float(0) 给默认值,unavailable 时得 0.0 #}
{{ states('sensor.outdoor_temp') | float(0) | round(1) }}
字符串拼接也是重灾区。HA 的模板里 + 对字符串和数字的行为容易搞混:
{# ❌ '当前温度 ' + 23.5 → 类型不匹配(字符串 + 浮点数) #}
{{ '当前温度 ' + states('sensor.temp') }}
{# ✅ 用 ~ 拼接,或先转字符串 #}
{{ '当前温度 ' ~ (states('sensor.temp') | float(0) | round(1)) ~ ' °C' }}
记住:+ 是算术/字符串相加,类型必须匹配;~ 是 Jinja 的字符串拼接操作符,会先转字符串再拼。模板里拼文本用 ~,别用 +。
还有 | int 的一个隐性坑:'12.7' | int 会得到 12(截断)而不是 13,'12.7' | round(0) 会得到 13.0。想要四舍五入别用 int,用 round:
{# ❌ 12.7 变成 12,不是 13 #}
{{ '12.7' | float | int }}
{# ✅ 12.7 保留 0 位 → 13.0,再 int → 13 #}
{{ ('12.7' | float | round(0)) | int }}
坑 3:过滤器用错或放错位置
报错:TemplateError: No filter named 'xxx' / 过滤结果不对
根因:过滤器名拼错,或把过滤器套在了不支持的类型上。
HA 继承 Jinja 全量过滤器,又加了一批自己的(round、as_timestamp、relative_time、float、int、now 等)。我踩过的典型错误:
{# ❌ 把 round 用在字符串上 → '3.14' | round 不存在,应该先转数字 #}
{{ '3.14' | round(1) }}
{# ✅ 先转数字再 round #}
{{ states('sensor.temp') | float(0) | round(1) }}
{# ❌ default 位置放错:default 要放在管道末尾,放中间会被前面的过滤器吃掉 #}
{{ states('sensor.x') | default('n/a') | float }}
{# ✅ default(true) 的 true 表示「undefined/None 也算作需要默认值」,放最后 #}
{{ states('sensor.x') | float(0) | round(1) | default('n/a', true) }}
常用过滤器速查(HA 专有 + Jinja 常用,我实际用到频率):
| 过滤器 | 作用 | 常见踩坑 |
|---|---|---|
float(default) |
转浮点 | 对 'unavailable' 直接炸,给默认值 |
int(default) |
转整数 | 同上,且截断不舍入 |
round(n) |
保留 n 位 | 只能用于数字,先转 |
default(value, true) |
默认值兜底 | 要放管道末尾 |
as_timestamp() |
转 Unix 秒 | 配合 now() 做差值 |
relative_time() |
相对时间 | 输入须是 datetime |
is_number()(测试) |
判断可转数字 | 转换前先判断 |
has_value()(测试) |
判断实体有值 | unavailable 时返回 false |
还有一个测试和过滤器的区分:is_number() 是测试,用 if x is number 或 x is number;float() 是过滤器,用 x | float。新手容易混:{{ states('sensor.x') is_number }} 是错的,应该 {{ states('sensor.x') is number }}。
坑 4:时间与时区——差 8 小时的神秘推手
报错:不报错,但结果永远差 8 小时 / 条件永远不满足 根因:把本地时区时间和 UTC 时间混着比。
这是不报错但功能错的典型。HA 里 now() 返回配置时区时间,utcnow() 返回 UTC,而实体的 last_changed 等时间戳是 UTC。直接比就出时区偏移:
{# ❌ now() 是本地时区,last_changed 是 UTC,直接比差 8 小时 #}
{% if now() - states.sensor.temp.last_changed > timedelta(minutes=5) %}
{# ✅ 统一转 as_timestamp 再比,绝对安全 #}
{% if (now().timestamp() - as_timestamp(states.sensor.temp.last_changed)) > 300 %}
修复原则:凡是做时间比较,统一走 as_timestamp() 转成 Unix 秒再算差值,别混用 now() 和 UTC 时间戳。格式化显示用 now().strftime('%H:%M') 没问题,但比较运算必须转成同一时区基准。
我线上一个真实案例:一个"传感器 10 分钟没更新就告警"的自动化,写的是 now() - states.sensor.x.last_changed > timedelta(minutes=10),结果白天从不触发,凌晨狂告警。查了半天发现 last_changed 是 UTC,now() 是东八区,差值算出来是"负数 8 小时 + 实际间隔",白天实际间隔不到 8 小时就永远不满足,凌晨过了 8 小时才满足。改成 as_timestamp() 统一后立刻正常。这种坑不报错,最磨人。
坑 5:state 属性访问与 unavailable 状态
报错:属性访问返回空/'unavailable',条件静默不满足
根因:实体存在但状态是 unavailable,或访问了不存在的属性。
这个坑最阴险——不抛错,就是静默。实体存在但离线(unavailable)时,states('sensor.x') 返回 'unavailable' 字符串,所有条件判断都会失败,自动化悄悄不触发:
{# ❌ unavailable 时不满足,自动化静默跳过,你还以为逻辑写错 #}
{% if states('sensor.motion') == 'on' %}
{# ✅ 先判断有值,再取状态 #}
{% if has_value('sensor.motion') and states('sensor.motion') == 'on' %}
{# ✅ 或判断实体状态不是 unavailable #}
{% if states('sensor.motion') != 'unavailable' and states('sensor.motion') == 'on' %}
属性访问同理。states.sensor.x.attributes.temperature 这种链式访问,任何一级缺失都会变 None/undefined。安全做法:访问属性前先确认实体有值,或者用 states.sensor.x.attributes.get('temperature', default)。
还有一类状态本身就是字符串的坑:sensor.energy 的状态是 '12.5'(字符串,带单位),states('sensor.energy') 返回 '12.5',直接 | float 没问题;但如果你拿它和数字比,比如 states('sensor.energy') | float > 10,注意 float 只转数字部分,带单位的状态(如 '12.5 kWh')会转失败。单位在 state 还是 attributes.unit_of_measurement,要看清楚再转。
六类报错决策树:从报错信息到根因
上面五类是常见坑,但真实报错往往混合出现。我给自己画了张决策树,先看报错类型,再看实体状态,最后定修复:
flowchart TD
A[模板报错/模板不生效] --> B{报错信息是什么?}
B -->|UndefinedError is undefined| C[实体不存在或拼错]
B -->|TypeError float/int| D[实体 unavailable 或类型不符]
B -->|No filter named| E[过滤器名拼错/类型不匹配]
B -->|无报错但结果差 8h| F[now 与 UTC 混用]
B -->|无报错但条件不满足| G[unavailable 或属性缺失]
B -->|预览通过但保存不生效| H[运行时实体状态与预览不同]
C --> C1[改用 states 函数式 + default]
D --> D1[先 is_number 判断 + float 兜底]
E --> E1[核对过滤器名 + 先转数字]
F --> F1[统一 as_timestamp 比较]
G --> G1[先 has_value 判断再取状态]
H --> H1[去日志看真实 Traceback]
实战排查顺序(我每次都这么走):
- 看日志:Settings → System → Logs,找
TemplateError或UndefinedError的完整 traceback——报错信息里有行号,能直接定位到模板哪一行。 - 验实体:Developer Tools → States,确认报错里提到的实体 ID 是否真实存在、状态是不是
unavailable。 - 修模板:按上面六类根因对症下药,函数式 + 判断 + 默认值三件套基本能覆盖九成。
- 重载:改完记得重载(sensor 改完进 Developer Tools → YAML → 重新加载,automation 改完在自动化页点重新加载)。
- 复验:Developer Tools → Template 用真实实体再渲染一次,确认输出是预期值,不是只看语法通过。
日志里怎么快速定位模板行号
日志里的 traceback 长这样,学会读它省一半时间:
2026-11-05 00:12:33 ERROR (MainThread) [homeassistant.components.template.template]
Error rendering template: UndefinedError: 'sensor.bedroom_temp' is undefined
File "/config/templates.yaml", line 12, in top-level template code
{{ states.sensor.bedroom_temp.state }}
最后一行 File "...", line 12 就是模板文件里的行号。去 templates.yaml 第 12 行看,基本一眼定位。记住:HA 的模板报错 traceback 是有文件+行号的,别在日志里只看第一行的错误信息就急着重写模板。
完整实战案例:一个告警模板从报错到修复
光讲规则容易忘,我拿一个真实走过的完整案例串一遍。那天我加了一个"空调不制冷告警"的模板自动化,逻辑是:客厅温度传感器超过 27℃ 且空调开着,就推送通知。第一次写:
automation:
- alias: "AC not cooling alert"
trigger:
- platform: state
entity_id: sensor.living_room_temp
condition:
- condition: template
value_template: "{{ states('sensor.living_room_temp') | float > 27 }}"
action:
- service: notify.mobile_app_phone
data:
title: "空调可能不制冷"
message: "客厅温度 {{ states('sensor.living_room_temp') }}°C,请检查空调"
跑起来当天没告警,我以为没到 27℃。第二天客厅 30℃,还是没告警。我打开 Developer Tools → Template 一渲染,直接炸:
去 Developer Tools → States 看 sensor.living_room_temp,状态是 unavailable——传感器离线了(Zigbee 协调器重启后没重新关联)。states() 返回了字符串 'unavailable',| float 无法转换,条件模板抛异常被当假处理,自动化静默跳过。
这就是坑 2 + 坑 5 的连击:传感器离线 → unavailable → float 炸 → 条件假 → 静默不触发。我改成了防错范式版本:
automation:
- alias: "AC not cooling alert"
trigger:
- platform: state
entity_id: sensor.living_room_temp
condition:
- condition: template
value_template: >
{% set t = states('sensor.living_room_temp') %}
{{ is_number(t) and (t | float) > 27 }}
action:
- service: notify.mobile_app_phone
data:
title: "空调可能不制冷"
message: "客厅温度 {{ states('sensor.living_room_temp') | float(0) | round(1) }}°C"
改完复验:is_number(t) 先挡住 unavailable,只有真数字才比较;消息里 float(0) 兜底,传感器离线时显示 0 而不是让整个动作失败。那次之后我立了条规矩:凡是模板里出现 states() 的结果做运算,前面必须有 is_number() 或 float(0) 兜底。
这个案例也说明模板排障的顺序为什么重要:先看实体状态(unavailable?),再看报错类型,最后才动模板。很多人一看到不触发就以为是自动化逻辑写错,其实实体都离线了。
生产模板防错范式
踩了这么多坑,我总结出一套防错范式,写模板时照着来,能避开大部分坑:
- 实体一律走函数式:
states('sensor.x'),不写states.sensor.x.state(缺实体不炸)。 - 数值先判断再转换:
states('sensor.x') | float(0)给默认值,或is_number()判断后再算。 - 默认值放管道末尾:
| default('n/a', true),永远最后。 - 时间比较统一基准:
as_timestamp()转 Unix 秒再比。 - 拼接用
~不用+:字符串拼接用~,类型安全。 - 访问属性前先确认有值:
has_value()或attributes.get()。 - 模板里不写复杂业务逻辑:超过三行就在 automation 里用
mode: restart+ 拆小步骤,别在一个模板里堆所有判断。 - 改完必重载、必复验:用真实实体渲染,确认输出值对,不是语法对。
{# 一个符合防错范式的生产级 template sensor 示例 #}
template:
- sensor:
- name: "Living room temp with trend"
state: >-
{% set t = states('sensor.living_room_temp') %}
{% if is_number(t) %}
{{ (t | float) | round(1) }}
{% else %}
n/a
{% endif %}
unit_of_measurement: "°C"
这段代码我线上在用:is_number() 先判,避免 unavailable 时 | float 炸;round(1) 保留一位;否则输出 n/a 而不是让传感器变 unknown。宁可显示 n/a,也不要让模板炸掉整条链路。
模板的缓存与刷新时机
还有一个不报错但结果不对的坑:模板何时重算。HA 的 template sensor 默认只在它引用的实体状态变化时重算。如果模板依赖的是系统时间(比如"每天 9 点切换模式")或一个长期不变的值,它可能永远不刷新:
template:
- sensor:
- name: "Daily mode"
# 依赖 now() 的模板,必须设 update_interval 才会周期性刷新
state: "{{ 'day' if now().hour >= 9 else 'night' }}"
update_interval:
minutes: 30
依赖 now() 的模板,务必设 update_interval,否则它只在状态变化时算一次,到点了不会自己切换。这个坑我见过有人用"每天定时重启 HA"来绕过,其实一个 update_interval 就解决。
常见 FAQ 补充
上面答案区已经列了六个最高频问题,这里补两个我常被问到的边界情况:
- 模板里的
for循环:遍历states.sensor | selectattr(...)这类写法,实体集很大时会拖慢渲染。HA 里states全局对象遍历代价高,能少遍历就少遍历,优先用states('sensor.x')点查。真的要聚合(比如求一组温度平均),用states.sensor | selectattr('state', 'ne', 'unavailable') | map(attribute='state') | map('float') | list,但实体超过几十个时建议改成在 automation 里聚合,别在模板里 for 循环。 - 模板的缩进与空白控制:Jinja 的
{%-和-%}控制空白,{%- if %}去掉前导空白,-%}去掉尾部。YAML 里模板缩进错了会整段解析失败,报的不是模板错误而是 YAML 错误,这种最容易让人懵——注意>和|块标量的缩进,模板内的内容必须比state:多缩进。
模板排障的核心就一句话:让模板对「实体不存在/不可用」免疫,再谈逻辑。把 default()、states() 函数式、is_number() 这三样用熟,九成模板报错都能当场解决。
相关教程:自动化基础 · 自动化 YAML 速查库 · HA 仪表盘 Lovelace · 自动化触发与条件 · HA 通知推送
官方文档:Home Assistant 模板文档 · Template 集成 · Jinja2 官方文档