mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
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:
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
+22
-9
@@ -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) {
|
||||||
if (t) return t.eid;
|
const t = tier.find((e) => isTempEntity(hass, e.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) {
|
||||||
if (found) return found.eid;
|
const found = tier.find((e) => e.eid.split('.')[0] === dom);
|
||||||
|
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). */
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user