fix v1.36.1: hidden grouped lamps toggle the lamp, not the DND switch

- primaryEntity: tiered selection, domain priority beats hidden flag
  (hidden light > visible config switch); visible same-domain still wins
- live-debugged on the real install: individual lamps hidden in the
  registry (light group setup) got switch.*_do_not_disturb / identify
  buttons as primary — tap toggle 'did nothing'
- +1 unit test (101); TESTING/CHANGELOG same-commit
This commit is contained in:
Matysh
2026-07-23 13:24:02 +03:00
parent d7a1b344e4
commit df2ed0c3e1
12 changed files with 109 additions and 19 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
FILES_URL = "/houseplan_files/files" FILES_URL = "/houseplan_files/files"
FILES_DIR = "houseplan/files" FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only" CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.36.0" VERSION = "1.36.1"
DEFAULT_CONFIG: dict = { DEFAULT_CONFIG: dict = {
"spaces": [], "spaces": [],
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues", "issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [], "requirements": [],
"single_config_entry": true, "single_config_entry": true,
"version": "1.36.0" "version": "1.36.1"
} }
+43
View File
@@ -0,0 +1,43 @@
import { launch } from './serve.mjs';
const { page, browser } = await launch();
const res = await page.evaluate(async () => {
const out = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const calls = [];
c.hass = { ...c.hass, callService: (d, s, data) => { calls.push([d, s, data.entity_id]); return Promise.resolve(); } };
// glow на текущем пространстве
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== c._space ? s : ({
...s, settings: { ...(s.settings || {}), fill_mode: 'glow' } })) };
c.requestUpdate(); await c.updateComplete;
out.mode = c._mode;
out.suppress = c._suppressClick;
out.holdFired = c._holdFired;
out.drag = !!c._drag;
// реальный клик по включённой лампе (элемент .dev)
const litDev = c._devices.find((d) => d.space === c._space && d.entities.some((e) => e.startsWith('light.') && c.hass.states[e]?.state === 'on'));
out.hasLit = !!litDev;
const el = [...sr().querySelectorAll('.dev')].find((e) => e.textContent.includes(litDev.name) || true);
// найдём элемент точно: по индексу устройства
const devEls = [...sr().querySelectorAll('.dev')];
out.devCount = devEls.length;
const target = devEls[c._devices.filter((d) => d.space === c._space).indexOf(litDev)] || devEls[0];
const before = calls.length;
c._infoCard = null;
// эмулируем полный цикл: pointerdown/up + click
const opts = { bubbles: true, composed: true };
target.dispatchEvent(new PointerEvent('pointerdown', { ...opts, pointerId: 5 }));
target.dispatchEvent(new PointerEvent('pointerup', { ...opts, pointerId: 5 }));
target.dispatchEvent(new MouseEvent('click', opts));
await c.updateComplete;
out.reaction = calls.length > before ? 'service:' + JSON.stringify(calls.at(-1)) : c._infoCard ? 'info' : 'NOTHING';
out.suppressAfter = c._suppressClick;
// и через прямой вызов
c._infoCard = null;
c._clickDevice(new MouseEvent('click'), litDev);
await c.updateComplete;
out.directReaction = calls.length > before + 1 ? 'service' : c._infoCard ? 'info' : 'NOTHING';
return out;
});
console.log(JSON.stringify(res, null, 1));
await browser.close();
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+9
View File
@@ -1,5 +1,14 @@
# Changelog # Changelog
## v1.36.1 — 2026-07-23
- Fixed tap-toggle "doing nothing" on lamps whose individual `light.*` entity
is **hidden in the registry** (the usual setup when lamps are folded into a
light group): the primary entity fell through to a visible config switch
(do-not-disturb) or an identify button, and the click toggled THAT.
Primary selection now works in tiers — domain priority beats hiddenness,
so a hidden light still wins over a visible config switch, while visible
entities of the same domain keep winning over hidden ones.
## v1.36.0 — 2026-07-23 (wall switches that really switch) ## v1.36.0 — 2026-07-23 (wall switches that really switch)
- Markers gained **"Controls light sources"**: bind any set of `light.*` / - Markers gained **"Controls light sources"**: bind any set of `light.*` /
`switch.*` entities to an icon. With tap action **Toggle**, a click flips `switch.*` entities to an icon. With tap action **Toggle**, a click flips
+4
View File
@@ -140,6 +140,10 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
(explicit ripple color still wins); off/white lights unchanged [auto] (explicit ripple color still wins); off/white lights unchanged [auto]
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any - [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static
- [ ] Hidden-light primary (v1.36.1): a lamp whose light entity is HIDDEN in
the registry (folded into a light group) still toggles/reflects the lamp,
not its do-not-disturb switch or identify button; visible entities of the
same domain still win over hidden ones [manual: click hallway lamps]
- [ ] Marker controls (v1.36.0): a marker with "Controls light sources" and - [ ] Marker controls (v1.36.0): a marker with "Controls light sources" and
tap action Toggle flips all bound lights/switches at once (any on → all tap action Toggle flips all bound lights/switches at once (any on → all
off, all off → all on, one service call); the icon and its RGB tint off, all off → all on, one service call); the icon and its RGB tint
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "houseplan-card", "name": "houseplan-card",
"version": "1.36.0", "version": "1.36.1",
"description": "Interactive house plan Lovelace card for Home Assistant", "description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
+20 -7
View File
@@ -53,20 +53,33 @@ export function isTempEntity(hass: any, eid: string): boolean {
} }
export function primaryEntity(hass: any, entIds: string[], icon: string): string | undefined { export function primaryEntity(hass: any, entIds: string[], icon: string): string | undefined {
const ents = entIds const all = entIds
.map((eid) => ({ eid, reg: hass.entities[eid], st: hass.states[eid] })) .map((eid) => ({ eid, reg: hass.entities[eid], st: hass.states[eid] }))
.filter((e) => e.reg && !e.reg.hidden); .filter((e) => e.reg);
const usable = ents.filter((e) => !e.reg.entity_category); // Tiers: visible primary entities first, but a HIDDEN light still beats a
const pool = usable.length ? usable : ents; // visible config switch. Real case: individual lamps folded into a light
// group get hidden in the registry — the device's main function is still
// the lamp, and tap-toggle must flip IT, not the do-not-disturb switch.
const tiers = [
all.filter((e) => !e.reg.hidden && !e.reg.entity_category),
all.filter((e) => !e.reg.hidden),
all.filter((e) => !e.reg.entity_category),
all,
];
if (icon === 'mdi:thermometer' || icon === 'mdi:air-filter') { if (icon === 'mdi:thermometer' || icon === 'mdi:air-filter') {
const t = pool.find((e) => isTempEntity(hass, e.eid)); for (const tier of tiers) {
const t = tier.find((e) => isTempEntity(hass, e.eid));
if (t) return t.eid; if (t) return t.eid;
} }
}
for (const dom of DOMAIN_PRIORITY) { for (const dom of DOMAIN_PRIORITY) {
const found = pool.find((e) => e.eid.split('.')[0] === dom); for (const tier of tiers) {
const found = tier.find((e) => e.eid.split('.')[0] === dom);
if (found) return found.eid; if (found) return found.eid;
} }
return pool[0]?.eid; }
for (const tier of tiers) if (tier.length) return tier[0].eid;
return undefined;
} }
/** Average zigbee LQI across the device's entities (*_linkquality/*_lqi sensors or an attribute). */ /** Average zigbee LQI across the device's entities (*_linkquality/*_lqi sensors or an attribute). */
+1 -1
View File
@@ -32,7 +32,7 @@ import './space-card';
import { cardStyles } from './styles'; import { cardStyles } from './styles';
import { langOf, t, type I18nKey } from './i18n'; import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.36.0'; const CARD_VERSION = '1.36.1';
const LS_KEY = 'houseplan_card_layout_v1'; const LS_KEY = 'houseplan_card_layout_v1';
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
const LS_ZOOM = 'houseplan_card_zoom_v1'; const LS_ZOOM = 'houseplan_card_zoom_v1';
+21
View File
@@ -309,3 +309,24 @@ test('areaHum: averages climate sensors only, integer %', () => {
assert.equal(areaHum(hass, devs, 'living'), 44); assert.equal(areaHum(hass, devs, 'living'), 44);
assert.equal(areaHum(hass, devs, 'nothing'), null); assert.equal(areaHum(hass, devs, 'nothing'), null);
}); });
test('primaryEntity: hidden light beats visible config switch (grouped lamps)', () => {
const hass = {
entities: {
'light.lamp': { hidden: true },
'switch.lamp_do_not_disturb': { entity_category: 'config' },
'button.lamp_identify': { entity_category: 'diagnostic' },
},
states: {},
};
assert.equal(
primaryEntity(hass, ['switch.lamp_do_not_disturb', 'light.lamp', 'button.lamp_identify'], 'mdi:lightbulb'),
'light.lamp',
);
// видимая сущность того же домена всё равно приоритетнее скрытой
const hass2 = {
entities: { 'light.a': { hidden: true }, 'light.b': {} },
states: {},
};
assert.equal(primaryEntity(hass2, ['light.a', 'light.b'], 'mdi:lightbulb'), 'light.b');
});