`}}fs.properties={_hdrH:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},fs.ZOOM_MAX=8,fs.ZOOM_MIN=.4,fs._touchSeen=!1,fs._noHoverMq="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,fs.styles=Wi,customElements.get("houseplan-card")||customElements.define("houseplan-card",fs),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.52.0 ","background:#3ea6ff;color:#04121f;font-weight:700","");
+ `}}fs.properties={_hdrH:{state:!0},hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},fs.ZOOM_MAX=8,fs.ZOOM_MIN=.4,fs._touchSeen=!1,fs._noHoverMq="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,fs.styles=Wi,customElements.get("houseplan-card")||customElements.define("houseplan-card",fs),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.52.1 ","background:#3ea6ff;color:#04121f;font-weight:700","");
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index cf0f5ab..e87e927 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## v1.52.1 — 2026-07-29
+
+**From the v1.52.0 review**
+
+- **A lit lamp always has exactly one indicator (HP-1520-01).** The glow
+ layer is hidden in the plan editor, but the yellow suppression still
+ applied there — a lit lamp showed neither the spot nor the badge. The
+ suppression gate now equals the layer's actual visibility: wherever the
+ spot is not drawn, the yellow badge returns.
+- **The static card honours a marker's size and rotation (HP-1513-01).** The
+ same stored marker rendered at base size, unrotated, on the read-only
+ card. It now mirrors the full card's --dev-scale and angle — geometry
+ only, still a schematic.
+- Documentation caught up with the v1.52.0 colour contract (HP-1520-02):
+ the RGB-tint expectations in TESTING/UX-MODES and a stale inline comment.
+
## v1.52.0 — 2026-07-29
**One look for light sources, whatever flipped them** (owner's rule)
diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md
index 9f0c23f..6e7c66e 100755
--- a/docs/CHANGELOG.ru.md
+++ b/docs/CHANGELOG.ru.md
@@ -6,6 +6,22 @@
> **Правило проекта:** оба файла пополняются в одном коммите с самим
> изменением — как и остальная документация (см. docs/STATUS.md).
+## v1.52.1 — 2026-07-29
+
+**По ревью v1.52.0**
+
+- **У горящей лампы всегда ровно один индикатор (HP-1520-01).** В редакторе
+ плана glow-слой скрыт, но подавление жёлтого там всё равно действовало —
+ горящая лампа не показывала ни пятна, ни подложки. Гейт подавления теперь
+ равен фактической видимости слоя: где пятно не рисуется, жёлтая подложка
+ возвращается.
+- **Статичная карточка уважает размер и поворот маркера (HP-1513-01).** Один
+ и тот же маркер рендерился на read-only карточке базовым и неповёрнутым.
+ Теперь она зеркалит --dev-scale и угол полной карточки — только геометрия,
+ по-прежнему схема.
+- Документация догнала цветовой контракт v1.52.0 (HP-1520-02): ожидания
+ RGB-окраски в TESTING/UX-MODES и устаревший комментарий в коде.
+
## v1.52.0 — 2026-07-29
**Один вид источников света, чем бы их ни переключали** (правило владельца)
diff --git a/docs/STATUS.md b/docs/STATUS.md
index 6c4dcef..eb8c189 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -15,12 +15,12 @@
| Item | State |
|---|---|
-| Version | **v1.52.0** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance |
+| Version | **v1.52.1** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance |
| Workflow | Since 2026-07-22: minor changes go to branch **`dev`** (build + smokes → deploy home → commit → push, NO release); releases are batched on the owner's command (merge dev→main, one tag, one release with a summary changelog, CI checked on dev beforehand) |
| GitHub | https://github.com/Matysh/houseplan-card — **`main` carries every published release, the latest tag is the current version above**; `dev` is where work lands and is merged into `main` at release time (so `dev` is normally equal to or ahead of `main`, never behind). Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
| CI | validate.yml (hacs + hassfest + frontend + backend) green; release.yml attaches the bundle on release publish |
| HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled; ~864 older open PRs but merge rate ≈180/mo; realistic ETA 1–3 months (checked 2026-07-24) |
-| Home instance | ha.jbstudio.pro (SSH port **22222**, key `ha_jb`; HA config root is `/mnt/data/supervisor/homeassistant` — `/config` does NOT exist in this SSH environment), deployed **v1.52.0** via direct copy (HACS custom repo also installed) |
+| Home instance | ha.jbstudio.pro (SSH port **22222**, key `ha_jb`; HA config root is `/mnt/data/supervisor/homeassistant` — `/config` does NOT exist in this SSH environment), deployed **v1.52.1** via direct copy (HACS custom repo also installed) |
| Localization | UI en/ru (src/i18n/*.json), everything user-visible localized incl. kiosk popover |
| Tests | Four layers: frontend unit (`npm test`, node:test over `test-build/`), pure backend (`pytest tests_backend`, runs anywhere), HA-harness backend (same folder, CI only — needs py3.13 + pytest-homeassistant-custom-component), and browser smokes (`demo/smoke_*.mjs`, headless chromium). **Counts are not written down here** — they went stale within two releases while the version line beside them was kept current, which reads as less coverage than exists (review R5-2). Run `npm run inventory` for the current numbers, or read them off the last CI run |
| Community | **Telegram chat: https://t.me/ha_houseplan** (created 2026-07-27) — the primary user-facing support channel; GitHub issues stay for bugs/features. Link it from any new release notes and posts |
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 5f376a7..19675a5 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -206,8 +206,10 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
lock locked↔unlocked, bulb on; custom icons and unavailable states never morph [manual]
- [ ] display "Value instead of an icon": the marker shows the measurement (°/%/unit)
as its body, small badges hidden; non-numeric fallback keeps the icon [manual]
-- [ ] RGB lights (v1.27.0): an on light with a color tints its icon/glow and the ripple
- (explicit ripple color still wins); off/white lights unchanged [manual]
+- [ ] RGB lights (v1.27.0, contract changed in v1.52.0): a lamp's colour lives
+ in its glow spot and the ripple fallback ONLY — the icon/badge/border get
+ no RGB tint; explicit ripple color still wins; off lights unchanged
+ [auto: smoke_light_badges + smoke_rgb_alarm]
- [ ] 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 [manual]; reduced-motion static
- [ ] Render cost (v1.43.1, audit L1): geometry (space model, open pairs) is
@@ -244,6 +246,12 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
at 70% of a device icon and zooming WITH the plan; the small metric rows
under the room name now show in the plan editor too
[auto: smoke_room_cards gearDetached/plainInPlan]
+- [ ] One indicator always (v1.52.1, HP-1520-01): in a glow space the plan
+ editor (no glow layer) shows the lit lamp yellow again; the devices
+ editor draws the layer and keeps the badge standard
+ [auto: smoke_light_badges]
+- [ ] Size/angle parity (v1.52.1, HP-1513-01): a marker with size 3 / angle 37
+ scales x3 and rotates on BOTH cards [auto: smoke_size_angle_parity]
- [ ] Light-source badges (v1.52.0): in glow fill a lit lamp's badge stays
standard (the spot is the indicator) and a lit socket stays yellow; in
other fills a lit lamp is plain yellow with no RGB tint; morphing and
@@ -565,8 +573,9 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
same domain still win over hidden ones [manual: click hallway lamps]
- [ ] 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
+ off, all off → all on, one service call); the icon state (yellow badge)
+ mirrors the targets, not the marker's own entity — the RGB tint is gone
+ since v1.52.0, target colours reach only the glow/ripple; 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: smoke_controls]
- [ ] Glow fill (v1.35.0): fill mode "Light sources" — every room painted with
diff --git a/docs/UX-MODES.md b/docs/UX-MODES.md
index f7219d5..14c27fe 100644
--- a/docs/UX-MODES.md
+++ b/docs/UX-MODES.md
@@ -76,7 +76,9 @@ Header in View: space tabs, device count, zoom cluster. Nothing else.
1. State-reflecting icons (open/closed door variants etc., like core HA).
2. `display: value` — show the measurement instead of an icon.
-3. Light color in the icon/ripple (RGB lights).
+3. Light color in the icon/ripple (RGB lights). *(Shipped in v1.27.0;
+ superseded in v1.52.0: the colour lives in the glow spot and the ripple
+ fallback only, the icon tint was removed by the owner's rule.)*
4. Alarm visual (leak/smoke/doorbell): red pulse overlay.
5. Rooms as sub-areas without an HA area + manual device placement by room id.
6. Backlog (not planned): music notes for players, directional TV ripples.
diff --git a/package-lock.json b/package-lock.json
index f7b2582..8416e8c 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "houseplan-card",
- "version": "1.52.0",
+ "version": "1.52.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "houseplan-card",
- "version": "1.52.0",
+ "version": "1.52.1",
"license": "MIT",
"dependencies": {
"lit": "^3.1.3",
diff --git a/package.json b/package.json
index c3c9f9f..a2d90c7 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
- "version": "1.52.0",
+ "version": "1.52.1",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts
index 91c4772..262b534 100755
--- a/src/houseplan-card.ts
+++ b/src/houseplan-card.ts
@@ -36,7 +36,7 @@ import { cardStyles } from './styles';
import { fitInSquare, contentBounds, spaceModels } from './space-geometry';
import { langOf, t, type I18nKey } from './i18n';
-const CARD_VERSION = '1.52.0';
+const CARD_VERSION = '1.52.1';
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';
@@ -4152,7 +4152,7 @@ class HouseplanCard extends LitElement {
${this._renderOpenings(disp)}
- ${devs.map((d) => this._renderDevice(d, view, showLqi, disp.fill === 'glow'))}
+ ${devs.map((d) => this._renderDevice(d, view, showLqi, disp.fill === 'glow' && !this._markup))}
${this._renderOpeningLocks(view)}
${disp.showNames || this._markup
? space.rooms.map((r) => this._renderRoomLabel(r, space, view, disp))
@@ -4210,11 +4210,13 @@ class HouseplanCard extends LitElement {
// and no live numbers either (HP-1510-02): no value text, no temperature,
// no humidity, no LQI badge, no state-morphed icon. The base icon and the
// name stay — enough to recognise the device and open its dialog.
- // The owner's rule for LIGHT SOURCES (2026-07-29): in glow fill the
- // indicator IS the glow spot — the badge stays standard, on or off; in
- // every other fill a lit source goes plain yellow like a heating TRV.
- // "Source" is exactly the litLightEntity condition that casts the spot,
- // so a lit socket or fan keeps its yellow even in glow fill.
+ // The owner's rule for LIGHT SOURCES (2026-07-29): where the glow layer
+ // is VISIBLE, the indicator IS the glow spot — the badge stays standard,
+ // on or off; wherever the spot is not drawn (other fills, the plan
+ // editor) a lit source goes plain yellow like a heating TRV. The gate
+ // must equal the layer's visibility, or a mode ends up with neither
+ // indicator (HP-1520-01). "Source" is exactly the litLightEntity
+ // condition that casts the spot, so a lit socket keeps its yellow.
let cls = d.hidden ? '' : this._stateClass(d);
if (glowFill && cls === 'on' && litLightEntity(this.hass, d)) cls = '';
const temp = d.hidden ? null : this._liveTemp(d);
@@ -4240,8 +4242,10 @@ class HouseplanCard extends LitElement {
const icon = this._config?.live_states && !d.hidden
? 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);
- // an icon with controlled targets takes the color of its first lit RGB target
+ // v1.52.0: a lamp's colour lives in its GLOW and in the ripple fallback
+ // only — the icon/border tint is gone. lightC is still computed (from the
+ // marker's first lit RGB target, else the primary light) purely to feed
+ // --ripple-color when no explicit ripple colour is set.
const ctrl = (m?.controls || []).filter(isControllable);
const lightC = this._config?.live_states && !d.hidden
? ctrl.length
diff --git a/src/space-render.ts b/src/space-render.ts
index d0ae71c..80d5a18 100644
--- a/src/space-render.ts
+++ b/src/space-render.ts
@@ -121,8 +121,15 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
const p = markerPos(d, o.layout, o.cfg, defPos, space);
const left = ((p.x - vb[0]) / vb[2]) * 100;
const top = ((p.y - vb[1]) / vb[3]) * 100;
- return html`
-
+ // per-marker size and rotation, exactly as on the full card (HP-1513-01):
+ // the same stored marker must look the same on both cards. Geometry only —
+ // no live-state dressing here, the static card stays a schematic.
+ const scale = Number(d.marker?.size) > 0 ? Number(d.marker!.size) : 1;
+ const angle = Number(d.marker?.angle) || 0;
+ const st = [`left:${left}%`, `top:${top}%`];
+ if (scale !== 1) st.push(`--dev-scale:${scale}`);
+ return html`