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:
+28
-13
@@ -17,7 +17,7 @@ import {
|
||||
pointOnBoundary, mergeRooms, splitRoom, polygonArea, closestPointOnBoundary,
|
||||
snapToWall, openingAmount,
|
||||
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
||||
stateIcon, lightColorOf, isAlarmState,
|
||||
stateIcon, lightColorOf, isAlarmState, parseRoomRef,
|
||||
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.27.0';
|
||||
const CARD_VERSION = '1.28.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';
|
||||
@@ -1575,7 +1575,9 @@ class HouseplanCard extends LitElement {
|
||||
link: d.link || '',
|
||||
description: d.description || '',
|
||||
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,
|
||||
};
|
||||
} else {
|
||||
@@ -1661,8 +1663,15 @@ class HouseplanCard extends LitElement {
|
||||
const res: { value: string; label: string }[] = [];
|
||||
for (const sp of this._serverCfg?.spaces || []) {
|
||||
for (const r of sp.rooms || []) {
|
||||
if (!r.area) continue;
|
||||
res.push({ value: sp.id + '#' + r.area, label: (sp.title || sp.id) + ' · ' + r.name });
|
||||
if (r.area) {
|
||||
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;
|
||||
@@ -1751,9 +1760,10 @@ class HouseplanCard extends LitElement {
|
||||
// determine the marker id
|
||||
let id: string;
|
||||
// a manually chosen room overrides the space/area for any icon
|
||||
const [roomSp, roomAr] = dlg.room ? dlg.room.split('#') : ['', ''];
|
||||
let space: string | null = roomSp || null;
|
||||
let area: string | null = roomAr || null;
|
||||
const roomRef = parseRoomRef(dlg.room);
|
||||
let space: string | null = roomRef?.space || null;
|
||||
let area: string | null = roomRef?.area || null;
|
||||
const roomId: string | null = roomRef?.roomId || null;
|
||||
if (dlg.binding === 'virtual' && !space) space = this._space;
|
||||
id = markerIdForBinding(dlg.binding, dlg.devId, () => 'v_' + Date.now().toString(36));
|
||||
const oldId = dlg.devId;
|
||||
@@ -1777,10 +1787,13 @@ class HouseplanCard extends LitElement {
|
||||
if (dlg.binding === 'virtual' || dlg.room) {
|
||||
marker.space = space;
|
||||
marker.area = area;
|
||||
marker.room_id = roomId;
|
||||
}
|
||||
// the room changed → move the icon to its center
|
||||
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)
|
||||
cfg.markers = cfg.markers.filter((m) => m.id !== id && m.id !== oldId);
|
||||
cfg.markers.push(marker);
|
||||
@@ -1792,10 +1805,12 @@ class HouseplanCard extends LitElement {
|
||||
const spm = this._spaceModel(space || undefined);
|
||||
let cx = spm.vb[0] + spm.vb[2] / 2;
|
||||
let cy = spm.vb[1] + spm.vb[3] / 2;
|
||||
if (area) {
|
||||
const room = spm.rooms.find((r) => r.area === area);
|
||||
if (room) [cx, cy] = this._roomCenter(room);
|
||||
}
|
||||
const room = roomId
|
||||
? spm.rooms.find((r) => r.id === roomId)
|
||||
: area
|
||||
? spm.rooms.find((r) => r.area === area)
|
||||
: undefined;
|
||||
if (room) [cx, cy] = this._roomCenter(room);
|
||||
newPos = this._normPos(space || this._space, cx, cy);
|
||||
this._layout = { ...this._layout, [id]: newPos };
|
||||
}
|
||||
|
||||
+2
-1
@@ -243,5 +243,6 @@
|
||||
"mode.view": "View",
|
||||
"mode.plan": "Plan",
|
||||
"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.plan": "План",
|
||||
"mode.devices": "Устройства",
|
||||
"display.value": "Значение вместо иконки"
|
||||
"display.value": "Значение вместо иконки",
|
||||
"marker.subarea": "без зоны, вручную"
|
||||
}
|
||||
|
||||
@@ -708,3 +708,27 @@ export function isAlarmState(
|
||||
if (domain === 'siren') return true;
|
||||
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;
|
||||
pdfs?: PdfRef[];
|
||||
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
|
||||
ripple_color?: string | null;
|
||||
ripple_size?: number | null; // max ring diameter, in icon diameters (default 3)
|
||||
|
||||
Reference in New Issue
Block a user