From 4ff35a69a1f7b3d9b63241ef23215f38585c6022 Mon Sep 17 00:00:00 2001 From: John Pettitt Date: Mon, 28 Sep 2026 18:46:24 -0700 Subject: [PATCH] Hide a null attribute with hide_unavailable (#452) Co-Authored-By: Claude Opus 5.5 Reviewed-by: John Pettitt @jpettitt --- CHANGELOG.md | 1 + README.md | 2 ++ src/index.test.js | 17 +++++++++++++++++ src/util.js | 4 +++- src/util.test.js | 11 +++++++++++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e0d02b..dc8b9c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. **Fixed:** - Templates in the row's own `tap_action` / `hold_action` / `double_tap_action` (e.g. a templated `navigation_path`) were passed raw to Home Assistant when tapping the row's icon or name, so navigation went to the literal Jinja. Only the main state value and additional entities resolved them (#460) +- `hide_unavailable` with an `attribute` now also hides when the attribute is `null`, the way integrations report "not set" - it showed as Unknown (or blank with a timestamp `format`) instead of hiding (#452) ## 4.12.0 diff --git a/README.md b/README.md index a1babe4..89686bf 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,8 @@ For example, only show the alarm exit-state sensor while the alarm is armed: `hide_if` and `hide_unavailable` at the top level hide the main entity's state value (the row itself stays visible); `default` is shown in its place when set. +With an `attribute` set, `hide_unavailable` also hides when that attribute is missing or `null` (which HA shows as Unknown); an empty list or string still shows. + ### Templating Display options accept Jinja **templates**, rendered by Home Assistant server-side and updated live whenever the entities they reference change. Any supported option whose value contains `{{ }}` or `{% %}` is treated as a template: `name`, `icon`, `icon_color`, `color`, `secondary_info` text, `hide_if`, a `template` option that replaces the displayed value entirely, any `styles` value, and any value inside `tap_action`/`hold_action`/`double_tap_action`. The `entity` variable holds the owning entity's id, and `vars` lets you name values reused across a row. diff --git a/src/index.test.js b/src/index.test.js index 65edeff..2eb2793 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -898,6 +898,23 @@ describe('multiple-entity-row', () => { expect(row.config.secondary_info).toBeUndefined(); }); + // The #452 follow-up: a null attribute rendered "Open: Unknown" instead of hiding. + it('hides an object-form secondary_info whose attribute is null', async () => { + el.setConfig({ + entity: 'binary_sensor.windows', + secondary_info: { attribute: 'open_windows', name: 'Open:', hide_unavailable: true }, + }); + el.hass = buildHass({ + 'binary_sensor.windows': { + entity_id: 'binary_sensor.windows', + state: 'off', + attributes: { open_windows: null }, + }, + }); + await flushRender(el); + expect(el.shadowRoot.querySelector('hui-generic-entity-row').secondaryText).toBeFalsy(); + }); + // Generic keywords are the one secondary_info HA renders natively - those must keep // passing through, with no secondaryText override. it('passes a generic secondary_info keyword through to the row config', async () => { diff --git a/src/util.js b/src/util.js index b949720..0c8fbd6 100644 --- a/src/util.js +++ b/src/util.js @@ -21,12 +21,14 @@ export const fireEvent = (node, type, detail) => { export const isUnavailable = (stateObj) => !stateObj || UNAVAILABLE_STATES.includes(stateObj.state); +// A null attribute counts as missing: integrations report "not set" that way, and HA displays it +// as Unknown (#452). An empty list or string is a real value and stays visible. export const hideUnavailable = (stateObj, config) => config.hide_unavailable && (isUnavailable(stateObj) || (config.attribute && ![LAST_CHANGED, LAST_UPDATED].includes(config.attribute) && - stateObj.attributes[config.attribute] === undefined)); + stateObj.attributes[config.attribute] == null)); export const hideIf = (stateObj, config, hass) => { if (hideUnavailable(stateObj, config)) { diff --git a/src/util.test.js b/src/util.test.js index 0c78697..6102a6e 100644 --- a/src/util.test.js +++ b/src/util.test.js @@ -74,6 +74,17 @@ describe('hideUnavailable', () => { const stateObj = { state: 'on', attributes: { brightness: 100 } }; expect(hideUnavailable(stateObj, { hide_unavailable: true, attribute: 'brightness' })).toBe(false); }); + + // #452: integrations report an unset attribute as null, which HA displays as Unknown. + it('is true when the configured attribute is null', () => { + const stateObj = { state: 'off', attributes: { open_windows: null } }; + expect(hideUnavailable(stateObj, { hide_unavailable: true, attribute: 'open_windows' })).toBe(true); + }); + + it.each([[[]], [''], [0], [false]])('is false for the present but empty-ish attribute %j', (value) => { + const stateObj = { state: 'off', attributes: { open_windows: value } }; + expect(hideUnavailable(stateObj, { hide_unavailable: true, attribute: 'open_windows' })).toBe(false); + }); }); describe('hideIf', () => {