mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
feat v1.28.0: sub-area rooms — manual device placement without an HA area (issue #3)
- area-less rooms appear in the marker room list ('no area, manual'); markers
store room_id and land at the room centre; dialog reopen restores the choice
- pure parseRoomRef (space#area / space#@roomId) + unit tests; backend schema
- ROADMAP phase 11 closed; TESTING.md row; smoke_subarea.mjs
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.27.0"
|
VERSION = "1.28.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.27.0"
|
"version": "1.28.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ MARKER_SCHEMA = vol.Schema(
|
|||||||
vol.Optional("link"): vol.Any(str, None),
|
vol.Optional("link"): vol.Any(str, None),
|
||||||
vol.Optional("description"): vol.Any(str, None),
|
vol.Optional("description"): vol.Any(str, None),
|
||||||
vol.Optional("tap_action"): vol.Any("info", "more-info", "toggle", None),
|
vol.Optional("tap_action"): vol.Any("info", "more-info", "toggle", None),
|
||||||
|
vol.Optional("room_id"): vol.Any(str, None),
|
||||||
vol.Optional("display"): vol.Any("badge", "ripple", "icon_ripple", None),
|
vol.Optional("display"): vol.Any("badge", "ripple", "icon_ripple", None),
|
||||||
vol.Optional("ripple_color"): vol.Any(str, None),
|
vol.Optional("ripple_color"): vol.Any(str, None),
|
||||||
vol.Optional("ripple_size"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=1, max=20)), None),
|
vol.Optional("ripple_size"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=1, max=20)), None),
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { launch } from './serve.mjs';
|
||||||
|
const { page, browser } = await launch();
|
||||||
|
const res = await page.evaluate(async () => {
|
||||||
|
const out = {};
|
||||||
|
const c = window.__card;
|
||||||
|
// комната без зоны на f1
|
||||||
|
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== 'f1' ? s : ({
|
||||||
|
...s, rooms: [...s.rooms, { id: 'rc', name: 'Cupboard', area: null, poly: [[0.05,0.62],[0.2,0.62],[0.2,0.9],[0.05,0.9]] }],
|
||||||
|
})) };
|
||||||
|
c._regSignature = ''; c._maybeRebuildDevices(); await c.updateComplete;
|
||||||
|
// список комнат в диалоге содержит подзону
|
||||||
|
c._setMode('devices');
|
||||||
|
c._openMarkerDialog(); await c.updateComplete;
|
||||||
|
const rooms = c._allRoomsFlat();
|
||||||
|
out.subareaListed = rooms.some((r) => r.value === 'f1#@rc');
|
||||||
|
// создать виртуальный маркер в подзону
|
||||||
|
c._markerDialog = { ...c._markerDialog, name: 'Vacuum dock', binding: 'virtual', room: 'f1#@rc' };
|
||||||
|
await c._saveMarker(); await c.updateComplete;
|
||||||
|
const m = c._serverCfg.markers.find((x) => x.name === 'Vacuum dock');
|
||||||
|
out.markerRoomId = m?.room_id;
|
||||||
|
out.markerAreaNull = m ? m.area === null : null;
|
||||||
|
// позиция — в центре подзоны (норм. центр rc: x=0.125, y=0.76)
|
||||||
|
const pos = c._layout[m.id];
|
||||||
|
out.posInRoom = pos && Math.abs(pos.x - 0.125) < 0.03 && Math.abs(pos.y - 0.76) < 0.03;
|
||||||
|
// девайс отрисован
|
||||||
|
const dev = c._devices.find((d) => d.id === m.id);
|
||||||
|
out.deviceBuilt = !!dev && dev.space === 'f1';
|
||||||
|
// reopen диалога восстанавливает выбор
|
||||||
|
c._openMarkerDialog(dev);
|
||||||
|
out.reopenRoom = c._markerDialog?.room;
|
||||||
|
c._markerDialog = null;
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(res, null, 1));
|
||||||
|
await browser.close();
|
||||||
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.28.0 — 2026-07-21 (sub-area rooms: manual placement without an HA area; issue #3)
|
||||||
|
- **Devices can now be placed into rooms that have no Home Assistant area.**
|
||||||
|
The marker dialog's room list includes area-less rooms (marked "no area,
|
||||||
|
manual"); pick one and the device/virtual marker lands at its centre. The
|
||||||
|
marker stores `room_id`, so the choice survives edits and re-opens. The
|
||||||
|
laundry-cupboard case from issue #3: a decorative room inside a larger area
|
||||||
|
can now hold its own door sensor and light.
|
||||||
|
- Room reference parsing extracted as pure `parseRoomRef` (`space#area` /
|
||||||
|
`space#@roomId`), unit-tested; backend schema for `room_id`.
|
||||||
|
|
||||||
## v1.27.0 — 2026-07-21 (RGB light colors + alarm pulse; issue #3)
|
## v1.27.0 — 2026-07-21 (RGB light colors + alarm pulse; issue #3)
|
||||||
- **RGB lights show their actual color**: an "on" light with a color tints its
|
- **RGB lights show their actual color**: an "on" light with a color tints its
|
||||||
bulb icon, glow and — unless a custom ripple color is set — its presence ripple.
|
bulb icon, glow and — unless a custom ripple color is set — its presence ripple.
|
||||||
|
|||||||
+5
-5
@@ -74,12 +74,12 @@ Track progress in `custom_components/houseplan/quality_scale.yaml` (done/exempt
|
|||||||
- [ ] GitHub: issue templates, discussions on, CONTRIBUTING.md (build/test in 5 minutes).
|
- [ ] GitHub: issue templates, discussions on, CONTRIBUTING.md (build/test in 5 minutes).
|
||||||
- [ ] Semantic versioning discipline; keep release notes user-facing (they show in HACS).
|
- [ ] Semantic versioning discipline; keep release notes user-facing (they show in HACS).
|
||||||
|
|
||||||
## Phase 11 — UX modes redesign (approved, see docs/UX-MODES.md)
|
## Phase 11 — UX modes redesign ✅ DONE (v1.25.0–v1.28.0, see docs/UX-MODES.md)
|
||||||
|
|
||||||
Three-tab interaction model (View / Plan / Devices): view mode is display-only —
|
Shipped: the three-tab interaction model (View / Plan / Devices, v1.25.0),
|
||||||
no dragging or editing; all editing splits between a Plan tab and a Devices tab.
|
state-reflecting icons + value display (v1.26.0), RGB light colors + alarm pulse
|
||||||
Deprecates "drag anywhere" and view-mode opening drag. Follow-ups from issue #3:
|
(v1.27.0), sub-area rooms (v1.28.0). Remaining backlog from issue #3: music
|
||||||
state-reflecting icons, value display, RGB light colors, alarm visuals, sub-area rooms.
|
notes for players, directional TV ripples (not planned).
|
||||||
|
|
||||||
## Explicit non-goals (for now)
|
## Explicit non-goals (for now)
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
|||||||
- [ ] Open via info card → Edit; all fields persist (name, icon, model, link, description)
|
- [ ] Open via info card → Edit; all fields persist (name, icon, model, link, description)
|
||||||
- [ ] Rebind to another device/entity/helper: search filters; already-placed candidates excluded; old position cleaned up [auto backend]
|
- [ ] Rebind to another device/entity/helper: search filters; already-placed candidates excluded; old position cleaned up [auto backend]
|
||||||
- [ ] Virtual device: requires name; room required; renders dashed
|
- [ ] Virtual device: requires name; room required; renders dashed
|
||||||
|
- [ ] Sub-area rooms (v1.28.0): a room WITHOUT an HA area appears in the marker
|
||||||
|
room list ("no area, manual"); a device placed there lands at its centre,
|
||||||
|
the marker stores room_id, reopening the dialog restores the choice [auto]
|
||||||
- [ ] Room override moves the icon to the room center
|
- [ ] Room override moves the icon to the room center
|
||||||
- [ ] Tap-action override select (default/info/more-info/toggle) saves and applies
|
- [ ] Tap-action override select (default/info/more-info/toggle) saves and applies
|
||||||
- [ ] PDF/manual upload: ok path; >50 MB → readable error; .exe → bad-ext error [auto backend]; traversal names sanitized [auto backend]
|
- [ ] PDF/manual upload: ok path; >50 MB → readable error; .exe → bad-ext error [auto backend]; traversal names sanitized [auto backend]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.27.0",
|
"version": "1.28.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",
|
||||||
|
|||||||
+28
-13
@@ -17,7 +17,7 @@ import {
|
|||||||
pointOnBoundary, mergeRooms, splitRoom, polygonArea, closestPointOnBoundary,
|
pointOnBoundary, mergeRooms, splitRoom, polygonArea, closestPointOnBoundary,
|
||||||
snapToWall, openingAmount,
|
snapToWall, openingAmount,
|
||||||
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
||||||
stateIcon, lightColorOf, isAlarmState,
|
stateIcon, lightColorOf, isAlarmState, parseRoomRef,
|
||||||
spaceDisplayOf, roomFillStyle, fillColorsOf, DEFAULT_FILL_COLORS, type FillColors,
|
spaceDisplayOf, roomFillStyle, fillColorsOf, DEFAULT_FILL_COLORS, type FillColors,
|
||||||
isActiveState, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY,
|
isActiveState, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY,
|
||||||
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
|
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
|
||||||
@@ -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.27.0';
|
const CARD_VERSION = '1.28.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';
|
||||||
@@ -1575,7 +1575,9 @@ class HouseplanCard extends LitElement {
|
|||||||
link: d.link || '',
|
link: d.link || '',
|
||||||
description: d.description || '',
|
description: d.description || '',
|
||||||
pdfs: [...(d.pdfs || [])],
|
pdfs: [...(d.pdfs || [])],
|
||||||
room: d.space && d.area ? d.space + '#' + d.area : '',
|
room: d.marker?.room_id
|
||||||
|
? d.space + '#@' + d.marker.room_id
|
||||||
|
: d.space && d.area ? d.space + '#' + d.area : '',
|
||||||
busy: false,
|
busy: false,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -1661,8 +1663,15 @@ class HouseplanCard extends LitElement {
|
|||||||
const res: { value: string; label: string }[] = [];
|
const res: { value: string; label: string }[] = [];
|
||||||
for (const sp of this._serverCfg?.spaces || []) {
|
for (const sp of this._serverCfg?.spaces || []) {
|
||||||
for (const r of sp.rooms || []) {
|
for (const r of sp.rooms || []) {
|
||||||
if (!r.area) continue;
|
if (r.area) {
|
||||||
res.push({ value: sp.id + '#' + r.area, label: (sp.title || sp.id) + ' · ' + r.name });
|
res.push({ value: sp.id + '#' + r.area, label: (sp.title || sp.id) + ' · ' + r.name });
|
||||||
|
} else if (r.id) {
|
||||||
|
// sub-area room (no HA area): manual placement by room id — issue #3
|
||||||
|
res.push({
|
||||||
|
value: sp.id + '#@' + r.id,
|
||||||
|
label: (sp.title || sp.id) + ' · ' + r.name + ' · ' + this._t('marker.subarea'),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
@@ -1751,9 +1760,10 @@ class HouseplanCard extends LitElement {
|
|||||||
// determine the marker id
|
// determine the marker id
|
||||||
let id: string;
|
let id: string;
|
||||||
// a manually chosen room overrides the space/area for any icon
|
// a manually chosen room overrides the space/area for any icon
|
||||||
const [roomSp, roomAr] = dlg.room ? dlg.room.split('#') : ['', ''];
|
const roomRef = parseRoomRef(dlg.room);
|
||||||
let space: string | null = roomSp || null;
|
let space: string | null = roomRef?.space || null;
|
||||||
let area: string | null = roomAr || null;
|
let area: string | null = roomRef?.area || null;
|
||||||
|
const roomId: string | null = roomRef?.roomId || null;
|
||||||
if (dlg.binding === 'virtual' && !space) space = this._space;
|
if (dlg.binding === 'virtual' && !space) space = this._space;
|
||||||
id = markerIdForBinding(dlg.binding, dlg.devId, () => 'v_' + Date.now().toString(36));
|
id = markerIdForBinding(dlg.binding, dlg.devId, () => 'v_' + Date.now().toString(36));
|
||||||
const oldId = dlg.devId;
|
const oldId = dlg.devId;
|
||||||
@@ -1777,10 +1787,13 @@ class HouseplanCard extends LitElement {
|
|||||||
if (dlg.binding === 'virtual' || dlg.room) {
|
if (dlg.binding === 'virtual' || dlg.room) {
|
||||||
marker.space = space;
|
marker.space = space;
|
||||||
marker.area = area;
|
marker.area = area;
|
||||||
|
marker.room_id = roomId;
|
||||||
}
|
}
|
||||||
// the room changed → move the icon to its center
|
// the room changed → move the icon to its center
|
||||||
const prevDev = oldId ? this._devices.find((x) => x.id === oldId) : null;
|
const prevDev = oldId ? this._devices.find((x) => x.id === oldId) : null;
|
||||||
const roomChanged = !!dlg.room && prevDev != null && (prevDev.space !== space || prevDev.area !== area);
|
const prevRoomId = prevDev?.marker?.room_id ?? null;
|
||||||
|
const roomChanged = !!dlg.room && prevDev != null
|
||||||
|
&& (prevDev.space !== space || prevDev.area !== area || prevRoomId !== roomId);
|
||||||
// remove the previous marker (by the old id and by the new id)
|
// remove the previous marker (by the old id and by the new id)
|
||||||
cfg.markers = cfg.markers.filter((m) => m.id !== id && m.id !== oldId);
|
cfg.markers = cfg.markers.filter((m) => m.id !== id && m.id !== oldId);
|
||||||
cfg.markers.push(marker);
|
cfg.markers.push(marker);
|
||||||
@@ -1792,10 +1805,12 @@ class HouseplanCard extends LitElement {
|
|||||||
const spm = this._spaceModel(space || undefined);
|
const spm = this._spaceModel(space || undefined);
|
||||||
let cx = spm.vb[0] + spm.vb[2] / 2;
|
let cx = spm.vb[0] + spm.vb[2] / 2;
|
||||||
let cy = spm.vb[1] + spm.vb[3] / 2;
|
let cy = spm.vb[1] + spm.vb[3] / 2;
|
||||||
if (area) {
|
const room = roomId
|
||||||
const room = spm.rooms.find((r) => r.area === area);
|
? spm.rooms.find((r) => r.id === roomId)
|
||||||
if (room) [cx, cy] = this._roomCenter(room);
|
: area
|
||||||
}
|
? spm.rooms.find((r) => r.area === area)
|
||||||
|
: undefined;
|
||||||
|
if (room) [cx, cy] = this._roomCenter(room);
|
||||||
newPos = this._normPos(space || this._space, cx, cy);
|
newPos = this._normPos(space || this._space, cx, cy);
|
||||||
this._layout = { ...this._layout, [id]: newPos };
|
this._layout = { ...this._layout, [id]: newPos };
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -243,5 +243,6 @@
|
|||||||
"mode.view": "View",
|
"mode.view": "View",
|
||||||
"mode.plan": "Plan",
|
"mode.plan": "Plan",
|
||||||
"mode.devices": "Devices",
|
"mode.devices": "Devices",
|
||||||
"display.value": "Value instead of an icon"
|
"display.value": "Value instead of an icon",
|
||||||
|
"marker.subarea": "no area, manual"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -243,5 +243,6 @@
|
|||||||
"mode.view": "Просмотр",
|
"mode.view": "Просмотр",
|
||||||
"mode.plan": "План",
|
"mode.plan": "План",
|
||||||
"mode.devices": "Устройства",
|
"mode.devices": "Устройства",
|
||||||
"display.value": "Значение вместо иконки"
|
"display.value": "Значение вместо иконки",
|
||||||
|
"marker.subarea": "без зоны, вручную"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -708,3 +708,27 @@ export function isAlarmState(
|
|||||||
if (domain === 'siren') return true;
|
if (domain === 'siren') return true;
|
||||||
return domain === 'binary_sensor' && !!deviceClass && ALARM_CLASSES.has(deviceClass);
|
return domain === 'binary_sensor' && !!deviceClass && ALARM_CLASSES.has(deviceClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- room references ----------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a marker's room reference. Two shapes:
|
||||||
|
* - `space#area` — a room bound to an HA area (historical form)
|
||||||
|
* - `space#@roomId` — a room WITHOUT an area (sub-area rooms, issue #3):
|
||||||
|
* devices are placed into it manually, by room id.
|
||||||
|
*/
|
||||||
|
export function parseRoomRef(
|
||||||
|
v: string | null | undefined,
|
||||||
|
): { space: string; area: string | null; roomId: string | null } | null {
|
||||||
|
if (!v) return null;
|
||||||
|
const i = v.indexOf('#');
|
||||||
|
if (i <= 0) return null;
|
||||||
|
const space = v.slice(0, i);
|
||||||
|
const rest = v.slice(i + 1);
|
||||||
|
if (!rest) return null;
|
||||||
|
if (rest.startsWith('@')) {
|
||||||
|
const roomId = rest.slice(1);
|
||||||
|
return roomId ? { space, area: null, roomId } : null;
|
||||||
|
}
|
||||||
|
return { space, area: rest, roomId: null };
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface Marker {
|
|||||||
description?: string | null;
|
description?: string | null;
|
||||||
pdfs?: PdfRef[];
|
pdfs?: PdfRef[];
|
||||||
tap_action?: string | null; // per-device override: 'info' | 'more-info' | 'toggle'
|
tap_action?: string | null; // per-device override: 'info' | 'more-info' | 'toggle'
|
||||||
|
room_id?: string | null; // manual placement into a room WITHOUT an HA area (sub-area rooms)
|
||||||
display?: 'badge' | 'ripple' | 'icon_ripple' | 'value' | null; // how the device is drawn
|
display?: 'badge' | 'ripple' | 'icon_ripple' | 'value' | null; // how the device is drawn
|
||||||
ripple_color?: string | null;
|
ripple_color?: string | null;
|
||||||
ripple_size?: number | null; // max ring diameter, in icon diameters (default 3)
|
ripple_size?: number | null; // max ring diameter, in icon diameters (default 3)
|
||||||
|
|||||||
+11
-1
@@ -4,7 +4,7 @@ import {
|
|||||||
lqiColor, snapToGrid, segKey, samePoint, pointInPolygon, markerIdForBinding, averageLqi,
|
lqiColor, snapToGrid, segKey, samePoint, pointInPolygon, markerIdForBinding, averageLqi,
|
||||||
fitView, declump, safeUrl, resolveTapAction, floorsOf, subst, spaceDisplayOf, roomFillColor,
|
fitView, declump, safeUrl, resolveTapAction, floorsOf, subst, spaceDisplayOf, roomFillColor,
|
||||||
segmentCm, formatLength, roomEdges, roomPoly, pointOnBoundary, pointStrictlyInside, roomsOverlap,
|
segmentCm, formatLength, roomEdges, roomPoly, pointOnBoundary, pointStrictlyInside, roomsOverlap,
|
||||||
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall, openingAmount, fillColorsOf, lerpColor, roomFillStyle, stateIcon, lightColorOf, isAlarmState,
|
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall, openingAmount, fillColorsOf, lerpColor, roomFillStyle, stateIcon, lightColorOf, isAlarmState, parseRoomRef,
|
||||||
} from '../test-build/logic.js';
|
} from '../test-build/logic.js';
|
||||||
import {
|
import {
|
||||||
iconFor, compileIconRules, isValidPattern, iconFromDeviceClasses,
|
iconFor, compileIconRules, isValidPattern, iconFromDeviceClasses,
|
||||||
@@ -513,3 +513,13 @@ test('isAlarmState: leak/smoke/gas/siren fire; doors and outages do not', () =>
|
|||||||
assert.ok(!isAlarmState('binary_sensor', 'smoke', 'off'));
|
assert.ok(!isAlarmState('binary_sensor', 'smoke', 'off'));
|
||||||
assert.ok(!isAlarmState('binary_sensor', 'smoke', 'unavailable'));
|
assert.ok(!isAlarmState('binary_sensor', 'smoke', 'unavailable'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parseRoomRef: area rooms, sub-area rooms by id, malformed refs', () => {
|
||||||
|
assert.deepEqual(parseRoomRef('f1#kitchen'), { space: 'f1', area: 'kitchen', roomId: null });
|
||||||
|
assert.deepEqual(parseRoomRef('f1#@r7'), { space: 'f1', area: null, roomId: 'r7' });
|
||||||
|
assert.equal(parseRoomRef(''), null);
|
||||||
|
assert.equal(parseRoomRef('f1#'), null);
|
||||||
|
assert.equal(parseRoomRef('f1#@'), null);
|
||||||
|
assert.equal(parseRoomRef('#kitchen'), null);
|
||||||
|
assert.equal(parseRoomRef(null), null);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user