Compare commits

...
9 Commits
Author SHA1 Message Date
Matysh 96a01e1380 v1.52.2 2026-07-29 22:19:22 +03:00
Matysh 08a9cc7d27 v1.52.2: the v1.52.1 review (HP-1521-01, HP-1521-02)
- HP-1521-01: the plan-mode assertion looked for ANY .dev.on and the lit
  kettle satisfied it — a false positive hiding the very regression it
  guards. It targets d_lamp now (kettle asserted separately), and the
  mutation check proves it: reverting the v1.52.1 gate fails the smoke.
- HP-1521-02: the checklist entry and the _stateClass comment still said
  'yellow in every fill mode'. Both now state the two-part contract: the
  state predicate is the glow-pool condition; the renderer keeps the badge
  only where the spot is not drawn.
2026-07-29 22:16:26 +03:00
Matysh 1be79a1427 v1.52.1 2026-07-29 21:58:05 +03:00
Matysh d7c20ff6a5 v1.52.1: the v1.52.0 review (HP-1520-01/-02, HP-1513-01)
- HP-1520-01: the glow layer is hidden in the plan editor, but the yellow
  suppression still fired there — a lit lamp had NEITHER indicator. The
  gate now equals the layer's visibility (disp.fill === 'glow' &&
  !this._markup), so the badge returns exactly where the spot is absent.
- HP-1513-01: the static card ignored marker.size and marker.angle — the
  same stored marker looked different on the two cards. It mirrors
  --dev-scale and the icon rotation now; geometry only, no live dressing.
- HP-1520-02: TESTING/UX-MODES still demanded the removed RGB icon tint,
  and the lightC comment described the old use. All three brought to the
  v1.52.0 contract.

smoke_light_badges grew the editor-mode vectors; new
smoke_size_angle_parity asserts the x3 ratio inside each card (absolute px
are incomparable across containers) and rotation on both. Inventory:
147 / 51 / 43 / 71.
2026-07-29 21:55:13 +03:00
Matysh fa59767c69 v1.52.0 2026-07-29 21:34:23 +03:00
Matysh 60d6167ecd v1.52.0: one look for light sources, whatever flipped them
Owner's rule, agreed 2026-07-29 after a field report (a lamp turned off by
tap looked different from one turned off by the wall switch):

- a lamp's colour lives ONLY in its glow. The v1.27 RGB tint of the icon,
  border and shadow is deleted — that tint was the fork: with colour data
  the lamp rendered dark-with-coloured-icon, without it plain yellow, and
  the same lamp crossed the fork depending on how it was switched.
- in glow fill the indicator IS the spot: a source's badge stays standard,
  lit or not (litLightEntity — the exact condition that casts the spot —
  gates the suppression, so a lit socket keeps its yellow even in glow).
- in every other fill a lit source is plain yellow, like a heating TRV.
- icon morphing stays everywhere; the ripple colour still falls back to the
  light colour (both explicitly confirmed by the owner).

smoke_light_badges covers the whole table (8 assertions); smoke_rgb_alarm
re-asserted: no rgb class, lit lamp yellow, ripple fallback keeps the
colour. README colour language updated. Inventory: 147 / 51 / 43 / 70.
2026-07-29 21:31:30 +03:00
Matysh 110fabd038 v1.51.3 2026-07-29 21:08:58 +03:00
Matysh 3456706ef6 v1.51.3: the size multiplier reaches the glyph (user report) 2026-07-29 21:06:10 +03:00
Matysh 1310c84a32 device icon: the size multiplier scales the glyph, not just the badge
User report via the owner: change a marker's size and the icon stays at its
default size — a big empty box around a small glyph. The badge, ripple and
value badges all derive from --dev-size (base size x per-device multiplier),
but --mdc-icon-size was pinned to the BASE --icon-size, so the multiplier
never reached the glyph. One calc argument: --dev-size.

New smoke_icon_scale: at size 3 the glyph grows with the badge and keeps
the 0.62 proportion. Inventory: 147 / 51 / 43 / 69.
2026-07-29 21:03:12 +03:00
21 changed files with 465 additions and 123 deletions
+3 -2
View File
@@ -78,8 +78,9 @@ Key advantages in short:
Icon colors follow one principle — **yellow means the device is doing its main job right now**:
a light is shining, a socket is powering, a fan is spinning, media is playing, a vacuum is
cleaning, a radiator valve is actually heating (not merely enabled). Orange = open / unlocked.
A pulsing red ring = an emergency (leak, smoke, gas). RGB bulbs color their icon with the real
light color. A translucent icon = unavailable. Dark = idle.
A pulsing red ring = an emergency (leak, smoke, gas). An RGB bulb's colour lives in its glow
spot (glow fill), where the spot itself is the on/off indicator and the badge stays standard.
A translucent icon = unavailable. Dark = idle.
- **Crisp zoom.** Zooming in does not "blur" the picture: the plan, labels and icons remain vector-sharp at any scale.
---
+2 -1
View File
@@ -77,7 +77,8 @@ House Plan показывает ваш умный дом так, как он в
Цвета значков подчиняются одному принципу — **жёлтый значит «устройство прямо сейчас выполняет свою основную работу»**:
лампа светит, розетка подаёт, вентилятор крутится, медиа играет, пылесос убирает, термоголовка
реально греет (а не просто включена). Оранжевый = открыто / не заперто. Пульсирующее красное
кольцо = авария (протечка, дым, газ). RGB-лампы окрашивают значок реальным цветом света.
кольцо = авария (протечка, дым, газ). Цвет RGB-лампы живёт в её пятне света (режим glow),
где само пятно — индикатор включения, а подложка значка остаётся стандартной.
Полупрозрачный значок = недоступно. Тёмный = покой.
- **Чёткий зум.** Приближение не «мылит» картинку: план, подписи и иконки остаются векторно-чёткими на любом масштабе.
+1 -1
View File
@@ -45,7 +45,7 @@ PLAN_ORPHAN_TTL_S = 3600
SCHEDULED_GRACE_S = 30 * 24 * 3600
FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.51.2"
VERSION = "1.52.2"
DEFAULT_CONFIG: dict = {
"spaces": [],
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.51.2"
"version": "1.52.2"
}
+38
View File
@@ -0,0 +1,38 @@
// Множитель «размер значка» обязан масштабировать ГЛИФ вместе с подложкой:
// --mdc-icon-size считался от базового --icon-size, и «увеличенный» значок
// был пустой рамкой вокруг дефолтной иконки (репорт пользователя, 2026-07-29).
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const out = await page.evaluate(async () => {
const o = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const d = c._devices.find((x) => x.space === 'f1' && x.bindingKind === 'device');
const glyphW = () => {
const el = [...sr().querySelectorAll('.dev')].find((e) => !e.classList.contains('ghost'));
const ic = el?.querySelector('ha-icon');
return ic ? ic.getBoundingClientRect().width : 0;
};
const badgeW = () => {
const el = [...sr().querySelectorAll('.dev')].find((e) => !e.classList.contains('ghost'));
return el ? el.getBoundingClientRect().width : 0;
};
// базовый размер
const g1 = glyphW(), b1 = badgeW();
// множитель ×3 на всех авто-устройствах через маркер первого
c._serverCfg.markers = (c._serverCfg.markers || []).filter((m) => m.id !== d.id);
c._serverCfg.markers.push({ id: d.id, binding: 'device:' + d.bindingRef, size: 3 });
c._cfgEpoch++; c._regSignature = '';
c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete;
await new Promise((r) => setTimeout(r, 100));
const scaled = [...sr().querySelectorAll('.dev')].find((e) => (e.getAttribute('style') || '').includes('--dev-scale:3'));
const g3 = scaled?.querySelector('ha-icon')?.getBoundingClientRect().width || 0;
const b3 = scaled?.getBoundingClientRect().width || 0;
o.badgeScales = b3 > b1 * 2.5;
o.glyphScalesWithBadge = g3 > g1 * 2.5; // раньше глиф оставался базовым
o.glyphFillsBadge = g3 / b3 > 0.45 && g3 / b3 < 0.8; // пропорция сохранена
c._serverCfg.markers = c._serverCfg.markers.filter((m) => m.id !== d.id);
c._cfgEpoch++; c._regSignature = ''; c._maybeRebuildDevices();
return o;
});
await finish(browser, checkAll(out));
+78
View File
@@ -0,0 +1,78 @@
// Правило владельца (2026-07-29): у источников света подложка в glow-режиме
// всегда стандартная — индикатор включения это ПЯТНО; в остальных режимах
// горящий источник жёлтый, как греющая термоголовка. RGB лампы красит только
// пятно (и фолбэк цвета пульсации); окраска иконки/рамки убрана. Морф иконки
// остаётся везде. Розетка в glow-режиме остаётся жёлтой — она не источник.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const out = await page.evaluate(async () => {
const o = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const setFill = async (mode) => {
const cfg = JSON.parse(JSON.stringify(c._serverCfg));
const f1 = cfg.spaces.find((s) => s.id === 'f1');
f1.settings = { ...(f1.settings || {}), fill_mode: mode };
c._serverCfg = cfg; c._cfgEpoch++; c._regSignature = '';
c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete;
await new Promise((r) => setTimeout(r, 80));
};
const lamp = c._devices.find((x) => x.id === 'd_lamp');
const kettle = c._devices.find((x) => x.id === 'd_kettle'); // розетка/чайник
// зажигаем лампу с RGB и розетку
c.hass = { ...c.hass, states: { ...c.hass.states,
[lamp.primary]: { ...c.hass.states[lamp.primary], state: 'on',
attributes: { ...(c.hass.states[lamp.primary]?.attributes || {}), rgb_color: [255, 120, 40] } },
[kettle.primary]: { ...c.hass.states[kettle.primary], state: 'on' } } };
const devEl = (id) => {
// надёжнее по индексу в списке devs: ищем по позиции через _pos
const d = c._devices.find((x) => x.id === id);
const v = c._viewOr(c._baseVb());
const p = c._pos(d);
const left = (((p.x - v.x) / v.w) * 100).toFixed(0);
return [...sr().querySelectorAll('.dev')].find((e) => Math.abs(parseFloat(e.style.left) - left) < 1.5);
};
// --- режим light: горящая RGB-лампа ЖЁЛТАЯ, без rgb-класса --------------
await setFill('light');
let le = devEl('d_lamp');
o.litLampYellowInLightMode = !!le && le.classList.contains('on') && !le.classList.contains('rgb');
o.morphInLightMode = le?.querySelector('ha-icon')?.getAttribute('icon')?.includes('lightbulb') ?? false;
// --- режим glow: лампа ТЁМНАЯ (индикатор — пятно), розетка жёлтая -------
await setFill('glow');
le = devEl('d_lamp');
const ke = devEl('d_kettle');
o.litLampDarkInGlow = !!le && !le.classList.contains('on') && !le.classList.contains('rgb');
o.glowSpotPresent = !!sr().querySelector('.stage svg radialGradient, .stage svg [id*=glow]');
o.socketStaysYellowInGlow = !!ke && ke.classList.contains('on');
o.morphInGlow = le?.querySelector('ha-icon')?.getAttribute('icon')?.includes('lightbulb') ?? false;
// --- HP-1520-01: в редакторах, где glow-слой скрыт, индикатор возвращается
// (лампа горит; в devices-редакторе слой рисуется — там лампа тёмная,
// в plan-редакторе слоя нет — там класс on должен вернуться)
c._setMode('devices'); c.requestUpdate(); await c.updateComplete;
le = devEl('d_lamp');
o.devicesEditorLampDark = !!le && !le.classList.contains('on');
o.devicesEditorHasGlow = !!sr().querySelector('.stage svg radialGradient, .stage svg [id*=glow]');
c._setMode('plan'); c.requestUpdate(); await c.updateComplete;
// ИМЕННО лампа (mutation-проверка аудита HP-1521-01: розетка тоже on и
// удовлетворяла бы поиску «любой .dev.on», пряча регресс самой лампы)
le = devEl('d_lamp');
o.planEditorLampYellow = !!le && le.classList.contains('on'); // слой скрыт — жёлтый вернулся
o.planEditorKettleStillOn = !!devEl('d_kettle')?.classList.contains('on');
o.planEditorNoGlow = !sr().querySelector('.stage svg radialGradient, .stage svg [id*=glow]');
c._setMode('view'); c.requestUpdate(); await c.updateComplete;
// --- выключенная лампа тёмная в обоих режимах ---------------------------
c.hass = { ...c.hass, states: { ...c.hass.states,
[lamp.primary]: { ...c.hass.states[lamp.primary], state: 'off' } } };
c.requestUpdate(); await c.updateComplete;
le = devEl('d_lamp');
o.offLampDarkInGlow = !!le && !le.classList.contains('on');
await setFill('light');
le = devEl('d_lamp');
o.offLampDarkInLightMode = !!le && !le.classList.contains('on');
return o;
});
await finish(browser, checkAll(out));
+14 -3
View File
@@ -8,9 +8,20 @@ const res = await page.evaluate(async () => {
c.hass = { ...c.hass, states: { ...c.hass.states,
'light.ceiling': { entity_id: 'light.ceiling', state: 'on', attributes: { friendly_name: 'Ceiling light', rgb_color: [255, 0, 128] } } } };
await c.updateComplete;
const rgbDev = sr().querySelector('.dev.rgb');
out.rgbClass = !!rgbDev;
out.rgbVar = rgbDev?.getAttribute('style')?.includes('--light-color:rgb(255, 0, 128)');
// v1.52.0 (правило владельца): RGB больше НЕ красит значок — цвет лампы
// живёт только в glow-пятне и в фолбэке цвета пульсации
out.rgbClassGone = !sr().querySelector('.dev.rgb');
out.litLampIsYellow = [...sr().querySelectorAll('.dev.on')].length > 0;
// фолбэк пульсации сохраняет цвет свечения: маркер icon_ripple на лампе
const lampDev = c._devices.find((x) => x.entities.includes('light.ceiling'));
c._serverCfg.markers = (c._serverCfg.markers || []).filter((m) => m.id !== lampDev.id);
c._serverCfg.markers.push({ id: lampDev.id, binding: 'device:' + lampDev.bindingRef, display: 'icon_ripple' });
c._cfgEpoch++; c._regSignature = ''; c._maybeRebuildDevices();
c.requestUpdate(); await c.updateComplete;
const rippleDev = [...sr().querySelectorAll('.dev')].find((e) => (e.getAttribute('style') || '').includes('--ripple-color'));
out.rippleKeepsLightColor = !!rippleDev && rippleDev.getAttribute('style').includes('rgb(255, 0, 128)');
c._serverCfg.markers = c._serverCfg.markers.filter((m) => m.id !== lampDev.id);
c._cfgEpoch++; c._regSignature = ''; c._maybeRebuildDevices(); await c.updateComplete;
// тревога: датчик протечки on
c.hass = { ...c.hass, states: { ...c.hass.states,
'binary_sensor.sink_leak': { entity_id: 'binary_sensor.sink_leak', state: 'on', attributes: { friendly_name: 'Leak', device_class: 'moisture' } } } };
+61
View File
@@ -0,0 +1,61 @@
// HP-1513-01: один и тот же маркер (size 3, angle 37) обязан выглядеть
// одинаково на полной и статичной карточках — статичная игнорировала и
// множитель размера, и поворот.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 900, height: 900 }, 1);
const out = await page.evaluate(async () => {
const o = {};
const c = window.__card;
await customElements.whenDefined('houseplan-space-card');
const d = c._devices.find((x) => x.space === 'f1' && x.bindingKind === 'device');
const cfg = JSON.parse(JSON.stringify(c._serverCfg));
cfg.markers = (cfg.markers || []).filter((m) => m.id !== d.id);
cfg.markers.push({ id: d.id, binding: 'device:' + d.bindingRef, size: 3, angle: 37 });
c._serverCfg = cfg; c._cfgEpoch++; c._regSignature = '';
c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete;
await new Promise((r) => setTimeout(r, 100));
const pick = (root) => [...root.querySelectorAll('.dev')]
.find((e) => (e.getAttribute('style') || '').includes('--dev-scale:3'));
const full = pick(c.shadowRoot || c.renderRoot);
const fW = full?.getBoundingClientRect().width || 0;
const fT = full ? getComputedStyle(full.querySelector('ha-icon')).transform : 'none';
window.__hpInvalidate?.();
const hass = { ...c.hass, callWS: async (m) => {
if (m.type === 'houseplan/config/get') return { config: cfg, rev: 83 };
if (m.type === 'houseplan/layout/get') return { layout: c._layout, rev: 83 };
return { ok: true };
}, connection: { subscribeEvents: async (cb) => { window.__hpInvalidate = cb; return () => {}; } } };
const host = document.createElement('div');
document.body.appendChild(host);
const card = document.createElement('houseplan-space-card');
card.setConfig({ type: 'custom:houseplan-space-card', space: 'f1' });
card.hass = hass;
host.appendChild(card);
const t0 = Date.now();
while (!pick(card.renderRoot || { querySelectorAll: () => [] }) && Date.now() - t0 < 6000) {
await new Promise((r) => setTimeout(r, 60));
}
const st = pick(card.renderRoot);
const sW = st?.getBoundingClientRect().width || 0;
const sT = st ? getComputedStyle(st.querySelector('ha-icon')).transform : 'none';
o.staticCarriesScale = sW > 0;
// абсолютные px несравнимы (разные контейнеры/letterbox) — контракт в том,
// что множитель РАБОТАЕТ на обеих карточках: базовый значок той же карточки
// втрое меньше масштабированного
const stBase = [...card.renderRoot.querySelectorAll('.dev')]
.find((e) => !(e.getAttribute('style') || '').includes('--dev-scale'));
const sBaseW = stBase?.getBoundingClientRect().width || 0;
o.staticRatioIs3 = sBaseW > 0 && Math.abs(sW / sBaseW - 3) < 0.35;
const fBase = [...(c.shadowRoot || c.renderRoot).querySelectorAll('.dev')]
.find((e) => !(e.getAttribute('style') || '').includes('--dev-scale') && !e.classList.contains('ghost'));
const fBaseW = fBase?.getBoundingClientRect().width || 0;
o.fullRatioIs3 = fBaseW > 0 && Math.abs(fW / fBaseW - 3) < 0.35;
o.bothRotated = fT !== 'none' && sT !== 'none' && fT.startsWith('matrix') && sT.startsWith('matrix');
if (!o.staticRatioIs3) console.log('static', sW, sBaseW);
host.remove();
c._serverCfg.markers = c._serverCfg.markers.filter((m) => m.id !== d.id);
c._cfgEpoch++; c._regSignature = ''; c._maybeRebuildDevices();
return o;
});
await finish(browser, checkAll(out));
File diff suppressed because one or more lines are too long
+27 -27
View File
File diff suppressed because one or more lines are too long
+55
View File
@@ -1,5 +1,60 @@
# Changelog
## v1.52.2 — 2026-07-29
**From the v1.52.1 review** (no runtime changes — test and wording quality)
- The plan-editor regression check now targets THE lamp (HP-1521-01): the
old assertion accepted any yellow badge, and the lit socket in the same
fixture would have satisfied it with the lamp fix removed. Verified by
mutation: reverting the v1.52.1 gate makes the smoke fail.
- The one remaining "yellow in every fill mode" wording — the checklist
entry and the _stateClass comment — now states the actual contract: the
lit-source state is computed by the glow-pool condition, and the yellow
badge shows only where the glow spot is not drawn (HP-1521-02).
## 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)
- **A lamp's colour lives only in its glow.** The RGB tint of the icon,
border and shadow is gone: depending on whether the state carried colour
data, the same lamp used to land in the "coloured icon on a dark badge" or
the "plain yellow badge" branch — turning one lamp off by tap and the rest
by the wall switch produced visibly different results. The branch is gone.
- **In glow fill the indicator IS the glow spot.** A light source's badge
stays standard, lit or not — the pool of light around it says everything.
A lit socket, fan or kettle keeps its yellow even in glow fill: they cast
no light, the rule is for sources only.
- **In every other fill a lit source is plain yellow**, like a heating
radiator valve — RGB and white lamps alike.
- Icon morphing (the shining-bulb outline) stays in every mode, and the
ripple colour still falls back to the lamp's light colour.
## v1.51.3 — 2026-07-29
- **The icon size multiplier scales the glyph, not just the badge.** Changing
a device's size grew the badge, the ripple and the value badges but left
the icon itself at its default size — a big empty box around a small glyph
(user report). The glyph now derives from the same per-device size as
everything else and keeps its proportion at any multiplier and zoom.
## v1.51.2 — 2026-07-29
**From the v1.51.1 review**
+56
View File
@@ -6,6 +6,62 @@
> **Правило проекта:** оба файла пополняются в одном коммите с самим
> изменением — как и остальная документация (см. docs/STATUS.md).
## v1.52.2 — 2026-07-29
**По ревью v1.52.1** (рантайм не менялся — качество теста и формулировок)
- Проверка регресса в редакторе плана целится ИМЕННО в лампу (HP-1521-01):
старый ассерт принимал любой жёлтый значок, и горящая розетка из той же
фикстуры прошла бы его даже без фикса лампы. Проверено мутацией: откат
гейта v1.52.1 валит смок.
- Последняя формулировка «жёлтый в любом режиме» — пункт чек-листа и
комментарий в _stateClass — приведена к фактическому контракту: состояние
источника считается условием glow-пула, а жёлтая подложка показывается
только там, где пятно не рисуется (HP-1521-02).
## 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
**Один вид источников света, чем бы их ни переключали** (правило владельца)
- **Цвет лампы живёт только в её свечении.** RGB-окраска иконки, рамки и
тени убрана: в зависимости от того, пришли ли с состоянием данные о
цвете, одна и та же лампа попадала то в ветку «цветная иконка на тёмной
подложке», то в «жёлтая подложка» — выключение одной лампы тапом и
остальных выключателем выглядело по-разному. Ветки больше нет.
- **В режиме glow индикатор — само пятно света.** Подложка источника всегда
стандартная, горит он или нет — всё говорит свет вокруг. Розетка,
вентилятор или чайник остаются жёлтыми и в glow: они не светят, правило —
только для источников.
- **В остальных режимах горящий источник — просто жёлтый**, как греющая
термоголовка; RGB и обычные лампы одинаково.
- Морфинг иконки (контур «лампа с лучами») остаётся во всех режимах, а цвет
пульсации по-прежнему берёт фолбэком цвет свечения лампы.
## v1.51.3 — 2026-07-29
- **Множитель «размер значка» масштабирует и сам глиф, а не только
подложку.** Изменение размера устройства растило подложку, пульсацию и
бейджи значений, но иконка внутри оставалась дефолтной — большая пустая
рамка вокруг маленького глифа (репорт пользователя). Теперь глиф считается
от того же поустройственного размера, что и всё остальное, и держит
пропорцию при любом множителе и зуме.
## v1.51.2 — 2026-07-29
**По ревью v1.51.1**
+2 -2
View File
@@ -15,12 +15,12 @@
| Item | State |
|---|---|
| Version | **v1.51.2** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance |
| Version | **v1.52.2** 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.51.2** 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.2** 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 |
+26 -7
View File
@@ -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,20 @@ 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
the ripple colour fallback survive [auto: smoke_light_badges +
smoke_rgb_alarm]
- [ ] Icon size multiplier scales the glyph (dev): set a marker's size to 3 —
the icon inside grows with the badge instead of staying default
[auto: smoke_icon_scale]
- [ ] Auto-grid parity (v1.51.2, HP-1511-01): with an empty layout, a visible
device among hidden ones sits at the same spot on both cards
[auto: smoke_hidden_flag autoGridParity]
@@ -267,9 +283,11 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
[auto: smoke_hidden_flag + unit seedHiddenBindings/seeded/legacy]
- [ ] Yellow means working (dev): a TRV whose hvac_action is heating glows
yellow; one that is merely enabled (idle) or has a service switch on
(anti-scaling, child lock) stays dark; a lit light yellows its icon in
every fill mode by the same condition that lights the glow pool
[auto: smoke_yellow_principle + unit primaryEntity/litLightEntity]
(anti-scaling, child lock) stays dark; a lit light turns its state on by
the same condition that lights the glow pool — and shows the yellow
badge only where the glow spot is NOT drawn: with the glow layer
visible the spot is the one indicator (v1.52.0)
[auto: smoke_yellow_principle + smoke_light_badges]
- [ ] Editor gestures on touch (dev): in the plan editor on a phone, pinch
zooms and a moving finger pans; releasing after a gesture does not draw
a point, a clean tap still does [auto: smoke_editor_gestures]
@@ -557,8 +575,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
+3 -1
View File
@@ -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.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "houseplan-card",
"version": "1.51.2",
"version": "1.52.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "houseplan-card",
"version": "1.51.2",
"version": "1.52.2",
"license": "MIT",
"dependencies": {
"lit": "^3.1.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.51.2",
"version": "1.52.2",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
+24 -11
View File
@@ -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.51.2';
const CARD_VERSION = '1.52.2';
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';
@@ -1213,9 +1213,12 @@ class HouseplanCard extends LitElement {
const controls = (d.marker?.controls || []).filter(isControllable);
if (controls.length)
return controls.some((e) => this.hass.states[e]?.state === 'on') ? 'on' : '';
// A shining light makes its icon yellow in EVERY fill mode — and it is
// the same condition that lights the glow pool, so the pool and the icon
// cannot disagree (they used to read different entities).
// A shining light yields 'on' here by the SAME condition that lights the
// glow pool, so the pool and the state cannot disagree. Note the renderer
// then STRIPS the class wherever the glow layer is actually visible —
// there the spot is the one indicator (v1.52.0, owner's rule); the badge
// stays yellow only where the spot is not drawn (other fills, the plan
// editor).
if (litLightEntity(this.hass, d)) return 'on';
const p = d.primary ? this.hass.states[d.primary] : undefined;
if (!p) return '';
@@ -4152,7 +4155,7 @@ class HouseplanCard extends LitElement {
${this._renderOpenings(disp)}
</svg>
<div class="devlayer" style="--icon-size:${((iconPct * vb[2] * (this._kiosk ? this._kioskScale.icon : 1)) / view.w).toFixed(3)}cqw;--rl-font:${this._kiosk ? this._kioskScale.font : 1}">
${devs.map((d) => this._renderDevice(d, view, showLqi))}
${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))
@@ -4202,7 +4205,7 @@ class HouseplanCard extends LitElement {
`;
}
private _renderDevice(d: DevItem, view: { x: number; y: number; w: number; h: number }, showLqi = true): TemplateResult {
private _renderDevice(d: DevItem, view: { x: number; y: number; w: number; h: number }, showLqi = true, glowFill = false): TemplateResult {
const p = this._pos(d);
const left = ((p.x - view.x) / view.w) * 100;
const top = ((p.y - view.y) / view.h) * 100;
@@ -4210,7 +4213,15 @@ 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.
const cls = d.hidden ? '' : this._stateClass(d);
// 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);
const hum = d.hidden ? null : this._liveHum(d);
const lqi = showLqi && !d.virtual && !d.hidden ? lqiFor(this.hass, d.entities) : null;
@@ -4234,8 +4245,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
@@ -4256,9 +4269,9 @@ class HouseplanCard extends LitElement {
if (m?.ripple_color) st.push(`--ripple-color:${m.ripple_color}`);
else if (lightC) st.push(`--ripple-color:${lightC}`);
}
if (lightC) st.push(`--light-color:${lightC}`);
return html`<div
class="dev ${cls} ${this._selId === d.id ? 'sel' : ''} ${d.virtual ? 'virtual' : ''} ${d.hidden ? 'ghost' : ''} ${disp === 'ripple' && !d.hidden ? 'noicon' : ''} ${valText != null ? 'valonly' : ''} ${lightC ? 'rgb' : ''} ${alarm ? 'alarm' : ''}"
class="dev ${cls} ${this._selId === d.id ? 'sel' : ''} ${d.virtual ? 'virtual' : ''} ${d.hidden ? 'ghost' : ''} ${disp === 'ripple' && !d.hidden ? 'noicon' : ''} ${valText != null ? 'valonly' : ''} ${alarm ? 'alarm' : ''}"
style="${st.join(';')}"
@click=${(e: MouseEvent) => this._clickDevice(e, d)}
@contextmenu=${(e: MouseEvent) => this._ctxDevice(e, d)}
+9 -2
View File
@@ -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`<div class="dev ${d.virtual ? 'virtual' : ''}" style="left:${left}%;top:${top}%">
<ha-icon icon="${d.icon}"></ha-icon>
// 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`<div class="dev ${d.virtual ? 'virtual' : ''}" style="${st.join(';')}">
<ha-icon icon="${d.icon}" style=${angle ? `transform:rotate(${angle}deg)` : nothing}></ha-icon>
</div>`;
});
+8 -8
View File
@@ -876,13 +876,9 @@ export const cardStyles = css`
white-space: nowrap;
}
/* RGB lights: the bulb takes the light's actual color */
.dev.rgb ha-icon { color: var(--light-color); }
.dev.rgb.on {
box-shadow: 0 0 10px var(--light-color);
border-color: var(--light-color);
background: var(--hp-bg);
color: var(--light-color);
}
/* v1.52.0: the RGB tint of the icon/border is gone — a lamp's colour
lives ONLY in its glow spot (owner's rule). The ripple-color fallback
keeps using the light colour; that is set inline via --ripple-color. */
/* alarms pulse red over everything */
.dev.alarm::after {
content: '';
@@ -939,7 +935,11 @@ export const cardStyles = css`
z-index: 2;
}
.dev ha-icon {
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62);
/* from --dev-size, NOT --icon-size: the per-device size multiplier must
scale the GLYPH with its badge. Pinned to the base size, "make this
icon bigger" grew an empty box around a default-size glyph (user
report via the owner, 2026-07-29). */
--mdc-icon-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.62);
display: flex;
align-items: center;
justify-content: center;