ITADN

Docs: states[k]?.state returns undefined for missing entities — Jinja vs JS asymmetry can silently flip aggregation logic

#1177Closededhartford 创建于 2026-05-11
E
edhartfordcommented
### What this is A documentation/awareness request, not a code bug. Posting here to help the next person who hits the same subtle bug pattern when porting aggregation logic between Jinja templates and `button-card` JS. ### Context When you read an entity state inside a button-card JS template via `states['my_entity']?.state`, the value is **`undefined`** if the entity isn't loaded in HA at all (entity disabled, integration removed, never created, etc.). This is correct JS optional-chaining behavior. For the **same** missing-entity case, the equivalent Jinja accessor (`states('my_entity')`) returns the **literal string `'unknown'`**. The asymmetry silently flips the semantics of a common defensive pattern that uses an inclusion check to "skip missing entities": ```js // button-card JS — looks defensive, actually BROKEN when entity is missing const keys = ['binary_sensor.foo', 'binary_sensor.bar']; let present = 0, ok = 0; for (const k of keys) { const st = states[k]?.state; if (!['unknown', 'unavailable', 'none', ''].includes(st)) { present++; if (st === 'on') ok++; } } ``` ```jinja {# Jinja — works correctly when entity is missing #} {%- set keys = ['binary_sensor.foo', 'binary_sensor.bar'] -%} {%- set ns = namespace(present=0, ok=0) -%} {%- for k in keys -%} {%- set st = states(k) -%} {%- if st not in ['unknown', 'unavailable', 'none', ''] -%} {%- set ns.present = ns.present + 1 -%} {%- if st == 'on' -%}{%- set ns.ok = ns.ok + 1 -%}{%- endif -%} {%- endif -%} {%- endfor -%} ``` Trace, JS version: - `states['binary_sensor.foo']` is `undefined` (entity not loaded) - `undefined?.state` is `undefined` - `!['unknown', 'unavailable', 'none', ''].includes(undefined)` is `!false` is `true` - So the loop body executes, `present++`, and the missing entity is counted as **present-but-not-OK** — exactly the case the inclusion check was meant to filter out. Jinja version: `states('binary_sensor.foo')` returns the string `'unknown'`, which IS in the inclusion list, so it gets correctly skipped. ### Reproduction Build a multi-entity device_tile that aggregates 2+ entities using the JS pattern above. Delete or disable one of the referenced integrations. The tile color/state will reflect "1/2 OK" (amber) instead of "1/1 OK" (green) — even though the second entity has simply ceased to exist. (Concrete example I hit: aggregating Tailscale node `key_expiry_disabled` binary_sensors. When one of the named nodes wasn't actually paired in HA, the JS tile read amber while the Jinja-equivalent tile on a different dashboard read green.) ### Fix in user code Add the `undefined` case to the skip: ```js for (const k of keys) { const s = states[k]; if (!s) continue; const st = s.state; if (['unknown', 'unavailable', 'none', ''].includes(st)) continue; present++; if (st === 'on') ok++; } ``` ### Suggested documentation update The button-card README's "Templates" section already notes JS templates have access to the full `states` object. Adding a brief note about the `undefined` return for missing entities (vs Jinja's `'unknown'` string) would save the next person a debugging round. Happy to PR a docs update if it'd be welcome — let me know the preferred section/wording. ### Environment - button-card: latest from HACS (custom_components/button-card) - Home Assistant Core: 2026.4.4 - Hit during a multi-dashboard refactor; the fix shipped as `if (!s) continue;` across all aggregating tiles in our setup.
关闭于 2026-05-11 2 条评论