mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 19:28:46 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96a01e1380 | ||
|
|
08a9cc7d27 | ||
|
|
1be79a1427 | ||
|
|
d7c20ff6a5 | ||
|
|
fa59767c69 | ||
|
|
60d6167ecd | ||
|
|
110fabd038 | ||
|
|
3456706ef6 | ||
|
|
1310c84a32 |
@@ -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
@@ -77,7 +77,8 @@ House Plan показывает ваш умный дом так, как он в
|
||||
Цвета значков подчиняются одному принципу — **жёлтый значит «устройство прямо сейчас выполняет свою основную работу»**:
|
||||
лампа светит, розетка подаёт, вентилятор крутится, медиа играет, пылесос убирает, термоголовка
|
||||
реально греет (а не просто включена). Оранжевый = открыто / не заперто. Пульсирующее красное
|
||||
кольцо = авария (протечка, дым, газ). RGB-лампы окрашивают значок реальным цветом света.
|
||||
кольцо = авария (протечка, дым, газ). Цвет RGB-лампы живёт в её пятне света (режим glow),
|
||||
где само пятно — индикатор включения, а подложка значка остаётся стандартной.
|
||||
Полупрозрачный значок = недоступно. Тёмный = покой.
|
||||
- **Чёткий зум.** Приближение не «мылит» картинку: план, подписи и иконки остаются векторно-чёткими на любом масштабе.
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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' } } } };
|
||||
|
||||
@@ -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
Vendored
+27
-27
File diff suppressed because one or more lines are too long
@@ -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**
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user