mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
feat v1.39.0: pure light sources toggle on click by default
- resolveTapAction: no explicit action + domain light -> toggle (kettle et al. keep info via their non-light primary); explicit choice wins - device dialog shows the effective default (defaultTap) - unit tests updated (+1, 106); smoke_light_default_tap.mjs; docs 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.38.4"
|
VERSION = "1.39.0"
|
||||||
|
|
||||||
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.38.4"
|
"version": "1.39.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { launch } from './serve.mjs';
|
||||||
|
const { page, browser } = await launch();
|
||||||
|
const res = await page.evaluate(async () => {
|
||||||
|
const out = {};
|
||||||
|
const c = window.__card;
|
||||||
|
const calls = [];
|
||||||
|
c.hass = { ...c.hass, callService: (d, s, data) => { calls.push([d, s, data.entity_id]); return Promise.resolve(); } };
|
||||||
|
await c.updateComplete;
|
||||||
|
c._setMode('view'); await c.updateComplete;
|
||||||
|
// лампа (primary light) без явного действия → клик = toggle
|
||||||
|
const lamp = c._devices.find((d) => d.primary?.startsWith('light.') && !d.tapAction);
|
||||||
|
out.hasLamp = !!lamp;
|
||||||
|
c._infoCard = null;
|
||||||
|
c._clickDevice(new MouseEvent('click'), lamp);
|
||||||
|
out.lampToggles = JSON.stringify(calls.at(-1)) === JSON.stringify(['homeassistant', 'toggle', lamp.primary]);
|
||||||
|
// устройство с не-light primary (сенсор) → клик = инфо
|
||||||
|
const sensorDev = c._devices.find((d) => d.primary?.startsWith('sensor.') && !d.tapAction);
|
||||||
|
const n = calls.length;
|
||||||
|
c._infoCard = null;
|
||||||
|
c._clickDevice(new MouseEvent('click'), sensorDev);
|
||||||
|
out.sensorInfo = calls.length === n && !!c._infoCard;
|
||||||
|
c._infoCard = null;
|
||||||
|
// явный info у лампы отключает дефолт
|
||||||
|
const cfgm = { id: lamp.id, binding: lamp.bindingKind + ':' + lamp.bindingRef, tap_action: 'info' };
|
||||||
|
c._serverCfg = { ...c._serverCfg, markers: [...(c._serverCfg.markers || []).filter((m) => m.id !== lamp.id), cfgm] };
|
||||||
|
c._regSignature = ''; c._maybeRebuildDevices(); await c.updateComplete;
|
||||||
|
const lamp2 = c._devices.find((d) => d.id === lamp.id);
|
||||||
|
const n2 = calls.length;
|
||||||
|
c._clickDevice(new MouseEvent('click'), lamp2);
|
||||||
|
out.explicitInfoWins = calls.length === n2 && !!c._infoCard;
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(res, null, 1));
|
||||||
|
await browser.close();
|
||||||
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.39.0 — 2026-07-23 (lights toggle by default)
|
||||||
|
- Pure light sources — devices whose primary entity is a `light` (bulbs,
|
||||||
|
chandeliers, night lights, light groups) — now **toggle on click by
|
||||||
|
default**, right from auto-placement, no per-device setting needed.
|
||||||
|
Devices where light is a side function (a kettle's backlight: its primary
|
||||||
|
is a sensor) keep the Device-card default. An explicit per-device choice
|
||||||
|
always wins. The device dialog shows the effective default.
|
||||||
|
|
||||||
## v1.38.4 — 2026-07-23
|
## v1.38.4 — 2026-07-23
|
||||||
- Plan editor: the DERIVED wall segments (the markup layer's solid lines)
|
- Plan editor: the DERIVED wall segments (the markup layer's solid lines)
|
||||||
are now trimmed under open boundaries as well — v1.38.3 only trimmed the
|
are now trimmed under open boundaries as well — v1.38.3 only trimmed the
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ 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
|
||||||
|
- [ ] Lights toggle by default (v1.39.0): a device whose PRIMARY entity is a
|
||||||
|
light (bulbs, chandeliers, night lights, light groups) toggles on click
|
||||||
|
out of the box — no per-device setting needed; the device dialog shows
|
||||||
|
"Toggle" as its effective default; devices where light is a side
|
||||||
|
function (kettle: primary = sensor) keep the Device-card default;
|
||||||
|
explicit per-device "Device card" wins over the default [auto]
|
||||||
- [ ] Derived walls cut too (v1.38.4): in the Plan editor the derived wall
|
- [ ] Derived walls cut too (v1.38.4): in the Plan editor the derived wall
|
||||||
segments (.seg) no longer run solid through an open stretch — only the
|
segments (.seg) no longer run solid through an open stretch — only the
|
||||||
dash remains there [auto]
|
dash remains there [auto]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.38.4",
|
"version": "1.39.0",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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.38.4';
|
const CARD_VERSION = '1.39.0';
|
||||||
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';
|
||||||
@@ -169,7 +169,8 @@ class HouseplanCard extends LitElement {
|
|||||||
rippleSize: number; // in icon diameters
|
rippleSize: number; // in icon diameters
|
||||||
size: number; // icon size multiplier
|
size: number; // icon size multiplier
|
||||||
angle: number; // icon rotation, degrees
|
angle: number; // icon rotation, degrees
|
||||||
tapAction: string; // '' = card default
|
tapAction: string; // '' = the effective default (defaultTap)
|
||||||
|
defaultTap: 'info' | 'toggle';
|
||||||
controls: string[]; // entities this icon toggles as a group
|
controls: string[]; // entities this icon toggles as a group
|
||||||
controlsFilter: string;
|
controlsFilter: string;
|
||||||
glowRadius: string; // per-device glow radius in display units; '' = global default
|
glowRadius: string; // per-device glow radius in display units; '' = global default
|
||||||
@@ -2151,6 +2152,7 @@ class HouseplanCard extends LitElement {
|
|||||||
size: Number(d.marker?.size) > 0 ? Number(d.marker!.size) : 1,
|
size: Number(d.marker?.size) > 0 ? Number(d.marker!.size) : 1,
|
||||||
angle: Number(d.marker?.angle) || 0,
|
angle: Number(d.marker?.angle) || 0,
|
||||||
tapAction: d.marker?.tap_action || '',
|
tapAction: d.marker?.tap_action || '',
|
||||||
|
defaultTap: d.primary?.split('.')[0] === 'light' ? 'toggle' : 'info',
|
||||||
controls: [...(d.marker?.controls || [])],
|
controls: [...(d.marker?.controls || [])],
|
||||||
controlsFilter: '',
|
controlsFilter: '',
|
||||||
glowRadius: Number(d.marker?.glow_radius_cm) > 0
|
glowRadius: Number(d.marker?.glow_radius_cm) > 0
|
||||||
@@ -2172,7 +2174,7 @@ class HouseplanCard extends LitElement {
|
|||||||
name: '', binding: 'virtual', bindingMode: 'virtual', bindingOpen: false,
|
name: '', binding: 'virtual', bindingMode: 'virtual', bindingOpen: false,
|
||||||
showEntities: false, bindingFilter: '', icon: '', autoIcon: '',
|
showEntities: false, bindingFilter: '', icon: '', autoIcon: '',
|
||||||
display: 'badge', rippleColor: '', rippleSize: 3, size: 1, angle: 0,
|
display: 'badge', rippleColor: '', rippleSize: 3, size: 1, angle: 0,
|
||||||
tapAction: '', controls: [], controlsFilter: '', glowRadius: '', model: '',
|
tapAction: '', defaultTap: 'info', controls: [], controlsFilter: '', glowRadius: '', model: '',
|
||||||
link: '', description: '', pdfs: [], room: '', busy: false,
|
link: '', description: '', pdfs: [], room: '', busy: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -4028,7 +4030,7 @@ class HouseplanCard extends LitElement {
|
|||||||
<select class="areasel"
|
<select class="areasel"
|
||||||
@change=${(e: Event) => (this._markerDialog = { ...d, tapAction: (e.target as HTMLSelectElement).value })}>
|
@change=${(e: Event) => (this._markerDialog = { ...d, tapAction: (e.target as HTMLSelectElement).value })}>
|
||||||
${[['info', 'tap.info'], ['more-info', 'tap.more_info'], ['toggle', 'tap.toggle']].map(
|
${[['info', 'tap.info'], ['more-info', 'tap.more_info'], ['toggle', 'tap.toggle']].map(
|
||||||
([v, k]) => html`<option value=${v} ?selected=${(d.tapAction || 'info') === v}>${this._t(k as any)}</option>`,
|
([v, k]) => html`<option value=${v} ?selected=${(d.tapAction || d.defaultTap) === v}>${this._t(k as any)}</option>`,
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -492,7 +492,11 @@ export function resolveTapAction(
|
|||||||
cardDefault: string | null | undefined,
|
cardDefault: string | null | undefined,
|
||||||
domain: string | null | undefined,
|
domain: string | null | undefined,
|
||||||
): TapAction {
|
): TapAction {
|
||||||
const want = explicit || cardDefault || 'info';
|
// Pure light sources (the device's PRIMARY function is a lamp: bulbs,
|
||||||
|
// chandeliers, night lights, light groups) toggle by default — no explicit
|
||||||
|
// setting needed. Devices where light is a side function (a kettle's
|
||||||
|
// backlight) have a non-light primary and keep the info default.
|
||||||
|
const want = explicit || cardDefault || (domain === 'light' ? 'toggle' : 'info');
|
||||||
if (want === 'more-info') return 'more-info';
|
if (want === 'more-info') return 'more-info';
|
||||||
if (want !== 'toggle') return 'info';
|
if (want !== 'toggle') return 'info';
|
||||||
if (!domain || TOGGLE_FORBIDDEN_DOMAINS.has(domain)) return 'info';
|
if (!domain || TOGGLE_FORBIDDEN_DOMAINS.has(domain)) return 'info';
|
||||||
|
|||||||
+15
-2
@@ -175,8 +175,9 @@ test('icon rules: device_class fallback', () => {
|
|||||||
assert.equal(iconFromDeviceClasses([]), null);
|
assert.equal(iconFromDeviceClasses([]), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tap action: defaults to info', () => {
|
test('tap action: defaults — info everywhere except pure lights (v1.39.0)', () => {
|
||||||
assert.equal(resolveTapAction(undefined, undefined, 'light'), 'info');
|
assert.equal(resolveTapAction(undefined, undefined, 'light'), 'toggle'); // лампы кликабельны из коробки
|
||||||
|
assert.equal(resolveTapAction(undefined, undefined, 'switch'), 'info');
|
||||||
assert.equal(resolveTapAction(null, 'info', 'switch'), 'info');
|
assert.equal(resolveTapAction(null, 'info', 'switch'), 'info');
|
||||||
assert.equal(resolveTapAction(null, 'more-info', 'sensor'), 'more-info');
|
assert.equal(resolveTapAction(null, 'more-info', 'sensor'), 'more-info');
|
||||||
});
|
});
|
||||||
@@ -721,3 +722,15 @@ test('outlineWithout: removes the cut stretch, keeps the rest', () => {
|
|||||||
// без вырезов — весь периметр
|
// без вырезов — весь периметр
|
||||||
assert.ok(Math.abs(outlineWithout(sq, []).reduce((a, s) => a + len(s), 0) - 40) < 1e-6);
|
assert.ok(Math.abs(outlineWithout(sq, []).reduce((a, s) => a + len(s), 0) - 40) < 1e-6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolveTapAction: pure lights toggle by default (v1.39.0)', () => {
|
||||||
|
assert.equal(resolveTapAction(null, null, 'light'), 'toggle');
|
||||||
|
assert.equal(resolveTapAction('', undefined, 'light'), 'toggle');
|
||||||
|
// явный выбор пользователя сильнее дефолта
|
||||||
|
assert.equal(resolveTapAction('info', null, 'light'), 'info');
|
||||||
|
// не-световые домены не трогаем
|
||||||
|
assert.equal(resolveTapAction(null, null, 'switch'), 'info');
|
||||||
|
assert.equal(resolveTapAction(null, null, 'sensor'), 'info');
|
||||||
|
// замок никогда
|
||||||
|
assert.equal(resolveTapAction('toggle', null, 'lock'), 'info');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user