mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
feat v1.36.0: marker controls — wall switches that really switch
- marker.controls[]: bound light.*/switch.* entities; explicit per-marker tap_action=toggle flips them with HA-group semantics in one call (controlsAction + isControllable pure helpers, +2 unit tests, 100) - icon state and RGB tint mirror the targets (stateless remotes and virtual dumb-switch markers finally show something); info card lists targets with states; locks filtered everywhere - chips+search UI in the marker dialog; backend schema; smoke_controls (9 checks); 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_DIR = "houseplan/files"
|
||||
CONF_ADMIN_ONLY = "admin_only"
|
||||
VERSION = "1.35.0"
|
||||
VERSION = "1.36.0"
|
||||
|
||||
DEFAULT_CONFIG: dict = {
|
||||
"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",
|
||||
"requirements": [],
|
||||
"single_config_entry": true,
|
||||
"version": "1.35.0"
|
||||
"version": "1.36.0"
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@ MARKER_SCHEMA = vol.Schema(
|
||||
vol.Optional("link"): vol.Any(str, None),
|
||||
vol.Optional("description"): vol.Any(str, None),
|
||||
vol.Optional("tap_action"): vol.Any("info", "more-info", "toggle", None),
|
||||
vol.Optional("controls"): vol.Any([str], None),
|
||||
vol.Optional("room_id"): vol.Any(str, None),
|
||||
vol.Optional("display"): vol.Any("badge", "ripple", "icon_ripple", None),
|
||||
vol.Optional("ripple_color"): vol.Any(str, None),
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
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(); } };
|
||||
// два подопытных light-entity
|
||||
const lights = Object.keys(c.hass.states).filter((e) => e.startsWith('light.')).slice(0, 2);
|
||||
out.twoLights = lights.length === 2;
|
||||
const setSt = async (m) => {
|
||||
const st = { ...c.hass.states };
|
||||
for (const [e, v] of Object.entries(m)) st[e] = { ...st[e], state: v };
|
||||
c.hass = { ...c.hass, states: st }; await c.updateComplete;
|
||||
};
|
||||
// виртуальный маркер-выключатель с controls и tap_action=toggle
|
||||
c._setMode('devices'); await c.updateComplete;
|
||||
c._openMarkerDialog();
|
||||
const room = c._spaceModel().rooms.find((r) => r.id && r.area);
|
||||
c._markerDialog = { ...c._markerDialog, name: 'Выключатель', binding: 'virtual',
|
||||
room: c._space + '#' + room.area, tapAction: 'toggle', controls: lights, icon: 'mdi:light-switch' };
|
||||
await c._saveMarker(); await c.updateComplete;
|
||||
const dev = c._devices.find((d) => d.name === 'Выключатель');
|
||||
out.markerSaved = !!dev && JSON.stringify(dev.marker.controls) === JSON.stringify(lights);
|
||||
c._setMode('view'); await c.updateComplete;
|
||||
// 1) все выключены → клик включает все
|
||||
await setSt({ [lights[0]]: 'off', [lights[1]]: 'off' });
|
||||
const dev2 = () => c._devices.find((d) => d.name === 'Выключатель');
|
||||
c._clickDevice(new MouseEvent('click'), dev2());
|
||||
out.turnOnAll = JSON.stringify(calls.at(-1)) === JSON.stringify(['homeassistant', 'turn_on', lights]);
|
||||
// 2) частично включено → клик выключает все
|
||||
await setSt({ [lights[0]]: 'on', [lights[1]]: 'off' });
|
||||
c._clickDevice(new MouseEvent('click'), dev2());
|
||||
out.partialTurnsOff = JSON.stringify(calls.at(-1)) === JSON.stringify(['homeassistant', 'turn_off', lights]);
|
||||
// 3) значок отражает цели: горит одна → класс on
|
||||
const el = [...sr().querySelectorAll('.dev')].find((e) => e.title === 'Выключатель' || e.textContent.includes(''));
|
||||
out.stateOn = !!c._stateClass(dev2()).includes('on');
|
||||
await setSt({ [lights[0]]: 'off' });
|
||||
out.stateOff = c._stateClass(dev2()) === '';
|
||||
// 4) без tap_action=toggle клик НЕ переключает (инфо)
|
||||
const cfg = c._serverCfg.markers.find((m) => m.name === 'Выключатель');
|
||||
cfg.tap_action = 'info'; c._regSignature = ''; c._maybeRebuildDevices(); await c.updateComplete;
|
||||
const n = calls.length;
|
||||
c._clickDevice(new MouseEvent('click'), dev2());
|
||||
out.infoNoToggle = calls.length === n && !!c._infoCard;
|
||||
// 5) инфо-карточка показывает цели
|
||||
await c.updateComplete;
|
||||
out.infoListsTargets = sr().querySelectorAll('.ctrlstate').length === 2;
|
||||
c._infoCard = null; await c.updateComplete;
|
||||
// 6) замок в controls отфильтрован при клике (защита)
|
||||
cfg.tap_action = 'toggle'; cfg.controls = ['lock.front_door', lights[0]];
|
||||
c._regSignature = ''; c._maybeRebuildDevices(); await c.updateComplete;
|
||||
await setSt({ [lights[0]]: 'off' });
|
||||
c._clickDevice(new MouseEvent('click'), dev2());
|
||||
const last = calls.at(-1);
|
||||
out.lockFiltered = JSON.stringify(last) === JSON.stringify(['homeassistant', 'turn_on', [lights[0]]]);
|
||||
return out;
|
||||
});
|
||||
console.log(JSON.stringify(res, null, 1));
|
||||
await browser.close();
|
||||
File diff suppressed because one or more lines are too long
Vendored
+66
-20
File diff suppressed because one or more lines are too long
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## v1.36.0 — 2026-07-23 (wall switches that really switch)
|
||||
- Markers gained **"Controls light sources"**: bind any set of `light.*` /
|
||||
`switch.*` entities to an icon. With tap action **Toggle**, a click flips
|
||||
them all with HA-group semantics — any on → all off, all off → all on — in
|
||||
one service call. Covers stateless remotes, one-switch-many-lights and
|
||||
dumb wall switches (place a virtual marker; no HA entity needed).
|
||||
- The icon mirrors its targets: on when any target is on, tinted by the first
|
||||
lit RGB light. The info card lists every target with its state. Controls
|
||||
fire only on the explicit per-marker Toggle (owner's decision); locks and
|
||||
other domains can never be group-controlled.
|
||||
|
||||
## v1.35.0 — 2026-07-23 (glow fill: dark house, glowing lamps)
|
||||
- New fill mode **"Light sources"**: the whole house is painted with a single
|
||||
configurable darkness color, and every lit lamp casts a radial pool of light
|
||||
|
||||
@@ -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]
|
||||
- [ ] 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
|
||||
- [ ] 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
|
||||
off, all off → all on, one service call); the icon and its RGB tint
|
||||
mirror the targets, not the marker's own entity; without explicit Toggle
|
||||
the click opens info as usual; the info card lists targets with states;
|
||||
locks/other domains are filtered out of controls [auto]
|
||||
- [ ] Glow fill (v1.35.0): fill mode "Light sources" — every room painted with
|
||||
one uniform darkness color; lit lamps glow with a radial gradient
|
||||
(rgb_color → color temp → default color; brightness scales opacity),
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "houseplan-card",
|
||||
"version": "1.35.0",
|
||||
"version": "1.36.0",
|
||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
+79
-6
@@ -17,7 +17,7 @@ import {
|
||||
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf,
|
||||
snapToWall, openingAmount,
|
||||
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
||||
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, glowColorOf, doorSector, hasRoomBehind,
|
||||
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, glowColorOf, doorSector, hasRoomBehind, controlsAction, isControllable,
|
||||
spaceDisplayOf, roomFillStyle, fillColorsOf, DEFAULT_FILL_COLORS, type FillColors,
|
||||
isActiveState, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY,
|
||||
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
|
||||
@@ -32,7 +32,7 @@ import './space-card';
|
||||
import { cardStyles } from './styles';
|
||||
import { langOf, t, type I18nKey } from './i18n';
|
||||
|
||||
const CARD_VERSION = '1.35.0';
|
||||
const CARD_VERSION = '1.36.0';
|
||||
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_ZOOM = 'houseplan_card_zoom_v1';
|
||||
@@ -166,6 +166,8 @@ class HouseplanCard extends LitElement {
|
||||
size: number; // icon size multiplier
|
||||
angle: number; // icon rotation, degrees
|
||||
tapAction: string; // '' = card default
|
||||
controls: string[]; // entities this icon toggles as a group
|
||||
controlsFilter: string;
|
||||
model: string;
|
||||
link: string;
|
||||
description: string;
|
||||
@@ -770,6 +772,11 @@ class HouseplanCard extends LitElement {
|
||||
|
||||
private _stateClass(d: DevItem): string {
|
||||
if (!this._config?.live_states) return '';
|
||||
// an icon with controlled targets mirrors THEM, not its own entity
|
||||
// (stateless remotes and virtual wall switches have nothing else to show)
|
||||
const controls = (d.marker?.controls || []).filter(isControllable);
|
||||
if (controls.length)
|
||||
return controls.some((e) => this.hass.states[e]?.state === 'on') ? 'on' : '';
|
||||
const p = d.primary ? this.hass.states[d.primary] : undefined;
|
||||
if (!p) return '';
|
||||
if (p.state === 'unavailable') return 'unavail';
|
||||
@@ -821,6 +828,17 @@ class HouseplanCard extends LitElement {
|
||||
return;
|
||||
}
|
||||
const domain = d.primary ? d.primary.split('.')[0] : null;
|
||||
// a switch with bound targets: the EXPLICIT per-marker toggle flips them
|
||||
// all with HA-group semantics (any on -> all off). Owner's decision:
|
||||
// controls never fire on the card-wide default action.
|
||||
const controls = (d.marker?.controls || []).filter(isControllable);
|
||||
if (d.tapAction === 'toggle' && controls.length) {
|
||||
const act = controlsAction(controls.map((e) => this.hass.states[e]?.state));
|
||||
this.hass
|
||||
.callService('homeassistant', act, { entity_id: controls })
|
||||
.catch((e: any) => this._showToast(this._t('toast.error', { err: this._errText(e) })));
|
||||
return;
|
||||
}
|
||||
const action = resolveTapAction(d.tapAction, this._config?.tap_action, domain);
|
||||
if (action === 'toggle' && d.primary) {
|
||||
this.hass
|
||||
@@ -1987,6 +2005,8 @@ class HouseplanCard extends LitElement {
|
||||
size: Number(d.marker?.size) > 0 ? Number(d.marker!.size) : 1,
|
||||
angle: Number(d.marker?.angle) || 0,
|
||||
tapAction: d.marker?.tap_action || '',
|
||||
controls: [...(d.marker?.controls || [])],
|
||||
controlsFilter: '',
|
||||
model: d.model || '',
|
||||
link: d.link || '',
|
||||
description: d.description || '',
|
||||
@@ -2000,7 +2020,7 @@ class HouseplanCard extends LitElement {
|
||||
this._markerDialog = {
|
||||
name: '', binding: 'virtual', bindingFilter: '', icon: '', autoIcon: '',
|
||||
display: 'badge', rippleColor: '', rippleSize: 3, size: 1, angle: 0,
|
||||
tapAction: '', model: '',
|
||||
tapAction: '', controls: [], controlsFilter: '', model: '',
|
||||
link: '', description: '', pdfs: [], room: '', busy: false,
|
||||
};
|
||||
}
|
||||
@@ -2194,6 +2214,7 @@ class HouseplanCard extends LitElement {
|
||||
size: dlg.size !== 1 ? dlg.size : null,
|
||||
angle: dlg.angle ? dlg.angle : null,
|
||||
tap_action: dlg.tapAction || null,
|
||||
controls: dlg.controls.length ? dlg.controls : null,
|
||||
model: dlg.model.trim() || null,
|
||||
link: dlg.link.trim() || null,
|
||||
description: dlg.description.trim() || null,
|
||||
@@ -3095,8 +3116,14 @@ class HouseplanCard extends LitElement {
|
||||
const icon = this._config?.live_states
|
||||
? stateIcon(d.icon, domain, primarySt?.attributes?.device_class, primarySt?.state, !!m?.icon)
|
||||
: d.icon;
|
||||
// RGB lights color the icon (and the ripple, unless a custom ripple color is set)
|
||||
const lightC = this._config?.live_states && domain === 'light' ? lightColorOf(primarySt) : null;
|
||||
// RGB lights color the icon (and the ripple, unless a custom ripple color is set);
|
||||
// an icon with controlled targets takes the color of its first lit RGB target
|
||||
const ctrl = (m?.controls || []).filter(isControllable);
|
||||
const lightC = this._config?.live_states
|
||||
? ctrl.length
|
||||
? ctrl.map((e) => lightColorOf(this.hass.states[e])).find((v) => v) || null
|
||||
: domain === 'light' ? lightColorOf(primarySt) : null
|
||||
: null;
|
||||
// emergencies (leak/smoke/gas/CO/siren) pulse red regardless of display mode
|
||||
const alarm = this._config?.live_states
|
||||
&& isAlarmState(domain, primarySt?.attributes?.device_class, primarySt?.state);
|
||||
@@ -3655,6 +3682,7 @@ class HouseplanCard extends LitElement {
|
||||
const d = this._infoCard!;
|
||||
const st = d.primary ? this.hass.states[d.primary] : undefined;
|
||||
const stateTxt = st ? this.hass.formatEntityState?.(st) ?? st.state : null;
|
||||
const controls = (d.marker?.controls || []).filter(isControllable);
|
||||
return html`<div class="menuwrap dialogwrap" @click=${() => (this._infoCard = null)}>
|
||||
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="hd"><ha-icon icon="${d.icon}"></ha-icon>${d.name}</div>
|
||||
@@ -3673,7 +3701,19 @@ class HouseplanCard extends LitElement {
|
||||
<ha-icon icon="mdi:file-pdf-box"></ha-icon>${p.name}</a>`,
|
||||
)}</span></div>`
|
||||
: nothing}
|
||||
${!d.model && !stateTxt && !d.link && !d.description && !(d.pdfs && d.pdfs.length)
|
||||
${controls.length
|
||||
? html`<div class="inforow"><span class="k">${this._t('info.controls')}</span>
|
||||
<span class="ctrlstates">
|
||||
${controls.map((eid) => {
|
||||
const cs = this.hass.states[eid];
|
||||
const on = cs?.state === 'on';
|
||||
return html`<span class="ctrlstate ${on ? 'on' : ''}">
|
||||
<ha-icon icon=${on ? 'mdi:lightbulb-on' : 'mdi:lightbulb-outline'}></ha-icon>
|
||||
${cs?.attributes?.friendly_name || eid}</span>`;
|
||||
})}
|
||||
</span></div>`
|
||||
: nothing}
|
||||
${!d.model && !stateTxt && !d.link && !d.description && !(d.pdfs && d.pdfs.length) && !controls.length
|
||||
? html`<div class="infodesc muted">${this._t('info.none')}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -3756,6 +3796,39 @@ class HouseplanCard extends LitElement {
|
||||
)}
|
||||
</select>
|
||||
|
||||
<label>${this._t('marker.controls_label')}</label>
|
||||
<div class="rhint">${this._t('marker.controls_hint')}</div>
|
||||
${d.controls.length
|
||||
? html`<div class="ctrlchips">
|
||||
${d.controls.map((eid) => html`<span class="ctrlchip">
|
||||
${this.hass.states[eid]?.attributes?.friendly_name || eid}
|
||||
<ha-icon icon="mdi:close" @click=${() =>
|
||||
(this._markerDialog = { ...d, controls: d.controls.filter((x) => x !== eid) })}></ha-icon>
|
||||
</span>`)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<input class="namein" type="text" placeholder=${this._t('marker.controls_filter')}
|
||||
.value=${d.controlsFilter}
|
||||
@input=${(e: Event) => (this._markerDialog = { ...d, controlsFilter: (e.target as HTMLInputElement).value })} />
|
||||
${d.controlsFilter.trim()
|
||||
? html`<div class="ctrllist">
|
||||
${Object.keys(this.hass.states)
|
||||
.filter((eid) => isControllable(eid) && !d.controls.includes(eid))
|
||||
.filter((eid) => {
|
||||
const q = d.controlsFilter.trim().toLowerCase();
|
||||
const name = String(this.hass.states[eid]?.attributes?.friendly_name || '');
|
||||
return eid.toLowerCase().includes(q) || name.toLowerCase().includes(q);
|
||||
})
|
||||
.slice(0, 8)
|
||||
.map((eid) => html`<button class="ctrlopt"
|
||||
@click=${() => (this._markerDialog = { ...d, controls: [...d.controls, eid], controlsFilter: '' })}>
|
||||
<ha-icon icon=${eid.startsWith('light.') ? 'mdi:lightbulb' : 'mdi:toggle-switch'}></ha-icon>
|
||||
${this.hass.states[eid]?.attributes?.friendly_name || eid}
|
||||
<span class="sub">${eid}</span>
|
||||
</button>`)}
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
<label>${this._t('marker.icon_label')}</label>
|
||||
${customElements.get('ha-icon-picker')
|
||||
? html`<ha-icon-picker .hass=${this.hass} .value=${d.icon}
|
||||
|
||||
+5
-1
@@ -287,5 +287,9 @@
|
||||
"gs.glow_light": "Default light color / intensity",
|
||||
"gs.glow_radius": "Glow radius",
|
||||
"gs.unit_m": "m",
|
||||
"gs.unit_ft": "ft"
|
||||
"gs.unit_ft": "ft",
|
||||
"marker.controls_label": "Controls light sources",
|
||||
"marker.controls_hint": "With tap action “Toggle”, a click flips all bound lights at once (any on → all off). The icon mirrors their state.",
|
||||
"marker.controls_filter": "Search lights and switches…",
|
||||
"info.controls": "Controls"
|
||||
}
|
||||
|
||||
+5
-1
@@ -287,5 +287,9 @@
|
||||
"gs.glow_light": "Цвет света по умолчанию / интенсивность",
|
||||
"gs.glow_radius": "Радиус свечения",
|
||||
"gs.unit_m": "м",
|
||||
"gs.unit_ft": "фут"
|
||||
"gs.unit_ft": "фут",
|
||||
"marker.controls_label": "Управляет источниками света",
|
||||
"marker.controls_hint": "При действии «Переключить» клик разом переключает все привязанные источники (горит хоть один → выключить все). Значок отражает их состояние.",
|
||||
"marker.controls_filter": "Поиск ламп и выключателей…",
|
||||
"info.controls": "Управляет"
|
||||
}
|
||||
|
||||
@@ -838,6 +838,19 @@ export function hasRoomBehind(
|
||||
return polys.some((poly) => pointStrictlyInside(p, poly, 1e-9));
|
||||
}
|
||||
|
||||
/**
|
||||
* Group toggle for a switch's controlled entities, HA-group semantics:
|
||||
* any target on -> turn everything off; all off -> turn everything on.
|
||||
*/
|
||||
export function controlsAction(states: (string | undefined)[]): 'turn_on' | 'turn_off' {
|
||||
return states.some((st) => st === 'on') ? 'turn_off' : 'turn_on';
|
||||
}
|
||||
|
||||
/** Only lights and plain switches may be group-controlled from the plan. */
|
||||
export function isControllable(entityId: string): boolean {
|
||||
return entityId.startsWith('light.') || entityId.startsWith('switch.');
|
||||
}
|
||||
|
||||
/** Device classes whose active state is an emergency, not a status. */
|
||||
const ALARM_CLASSES = new Set(['smoke', 'gas', 'carbon_monoxide', 'moisture', 'safety', 'tamper', 'problem']);
|
||||
|
||||
|
||||
@@ -396,6 +396,26 @@ export const cardStyles = css`
|
||||
display: inline-flex;
|
||||
}
|
||||
.roomlabel .rlm.lit { opacity: 1; }
|
||||
.ctrlchips { display: flex; flex-wrap: wrap; gap: 5px; margin: 4px 0; }
|
||||
.ctrlchip {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
background: var(--hp-accent); color: var(--text-primary-color, #fff);
|
||||
border-radius: 12px; padding: 3px 8px; font-size: 12px;
|
||||
}
|
||||
.ctrlchip ha-icon { --mdc-icon-size: 14px; cursor: pointer; }
|
||||
.ctrllist { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
|
||||
.ctrlopt {
|
||||
display: flex; align-items: center; gap: 7px; text-align: left;
|
||||
border: 0; background: transparent; color: var(--hp-txt);
|
||||
padding: 5px 7px; border-radius: 6px; cursor: pointer; font-family: inherit; font-size: 13px;
|
||||
}
|
||||
.ctrlopt:hover { background: var(--secondary-background-color, rgba(128,128,128,0.15)); }
|
||||
.ctrlopt .sub { color: var(--hp-muted); font-size: 11px; margin-left: auto; }
|
||||
.ctrlopt ha-icon { --mdc-icon-size: 16px; }
|
||||
.ctrlstates { display: flex; flex-direction: column; gap: 3px; }
|
||||
.ctrlstate { display: inline-flex; align-items: center; gap: 5px; color: var(--hp-muted); }
|
||||
.ctrlstate.on { color: var(--hp-txt); }
|
||||
.ctrlstate ha-icon { --mdc-icon-size: 15px; }
|
||||
.iconauto {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -44,6 +44,8 @@ export interface Marker {
|
||||
ripple_size?: number | null; // max ring diameter, in icon diameters (default 3)
|
||||
size?: number | null; // icon size multiplier (default 1)
|
||||
angle?: number | null; // icon rotation, degrees
|
||||
/** Entities this icon toggles as a group (wall switch → its lights). */
|
||||
controls?: string[] | null;
|
||||
}
|
||||
|
||||
/** A door or window: plan geometry (normalized coords), optionally live via entities. */
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
fitView, declump, safeUrl, resolveTapAction, floorsOf, subst, spaceDisplayOf, roomFillColor,
|
||||
splitRoomPath, polyContainsPoly, islandsOf,
|
||||
kelvinToRgb, glowColorOf, doorSector, hasRoomBehind,
|
||||
controlsAction, isControllable,
|
||||
segmentCm, formatLength, roomEdges, roomPoly, pointOnBoundary, pointStrictlyInside, roomsOverlap,
|
||||
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall, openingAmount, fillColorsOf, lerpColor, roomFillStyle, stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices,
|
||||
} from '../test-build/logic.js';
|
||||
@@ -649,3 +650,19 @@ test('hasRoomBehind: neighbour room yes, street no', () => {
|
||||
assert.ok(!hasRoomBehind([10, 0], 90, [5, 0], [], 1));
|
||||
assert.ok(!hasRoomBehind([10, 0], 90, [5, 0], [[[30, 30], [40, 30], [40, 40], [30, 40]]], 1));
|
||||
});
|
||||
|
||||
test('controlsAction: HA-group semantics', () => {
|
||||
assert.equal(controlsAction(['off', 'off']), 'turn_on');
|
||||
assert.equal(controlsAction(['off', 'on']), 'turn_off');
|
||||
assert.equal(controlsAction(['on', 'on']), 'turn_off');
|
||||
assert.equal(controlsAction([undefined, 'off']), 'turn_on');
|
||||
assert.equal(controlsAction([]), 'turn_on');
|
||||
});
|
||||
|
||||
test('isControllable: lights and switches only', () => {
|
||||
assert.ok(isControllable('light.kitchen'));
|
||||
assert.ok(isControllable('switch.pump'));
|
||||
assert.ok(!isControllable('lock.front'));
|
||||
assert.ok(!isControllable('cover.gate'));
|
||||
assert.ok(!isControllable('alarm_control_panel.home'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user