mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
feat v1.36.2: per-source glow radius
- marker.glow_radius_cm overrides the global default per device (field in the marker dialog, HA units, placeholder = global default); pools and door sectors use the per-source radius - backend schema; smoke_glow extended (11 checks); docs same-commit
This commit is contained in:
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
|
|||||||
FILES_URL = "/houseplan_files/files"
|
FILES_URL = "/houseplan_files/files"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.36.1"
|
VERSION = "1.36.2"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"spaces": [],
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -16,5 +16,5 @@
|
|||||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"version": "1.36.1"
|
"version": "1.36.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ MARKER_SCHEMA = vol.Schema(
|
|||||||
vol.Optional("description"): vol.Any(str, None),
|
vol.Optional("description"): vol.Any(str, None),
|
||||||
vol.Optional("tap_action"): vol.Any("info", "more-info", "toggle", None),
|
vol.Optional("tap_action"): vol.Any("info", "more-info", "toggle", None),
|
||||||
vol.Optional("controls"): vol.Any([str], None),
|
vol.Optional("controls"): vol.Any([str], None),
|
||||||
|
vol.Optional("glow_radius_cm"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=10, max=10000)), None),
|
||||||
vol.Optional("room_id"): vol.Any(str, None),
|
vol.Optional("room_id"): vol.Any(str, None),
|
||||||
vol.Optional("display"): vol.Any("badge", "ripple", "icon_ripple", None),
|
vol.Optional("display"): vol.Any("badge", "ripple", "icon_ripple", None),
|
||||||
vol.Optional("ripple_color"): vol.Any(str, None),
|
vol.Optional("ripple_color"): vol.Any(str, None),
|
||||||
|
|||||||
@@ -73,6 +73,17 @@ const res = await page.evaluate(async () => {
|
|||||||
c.requestUpdate(); await c.updateComplete;
|
c.requestUpdate(); await c.updateComplete;
|
||||||
const clips2 = [...sr().querySelectorAll('defs clipPath[id^="hp-glowclip"] path')];
|
const clips2 = [...sr().querySelectorAll('defs clipPath[id^="hp-glowclip"] path')];
|
||||||
out.entranceNoSector = clips2.every((p) => ((p.getAttribute('d') || '').match(/M /g) || []).length === 1);
|
out.entranceNoSector = clips2.every((p) => ((p.getAttribute('d') || '').match(/M /g) || []).length === 1);
|
||||||
|
// 7а) персональный радиус источника перекрывает глобальный
|
||||||
|
const litMarkerId = litLight.id;
|
||||||
|
c._serverCfg = { ...c._serverCfg, markers: [
|
||||||
|
...(c._serverCfg.markers || []).filter((m) => m.id !== litMarkerId),
|
||||||
|
{ id: litMarkerId, binding: litLight.bindingKind === 'virtual' ? 'virtual' : litLight.bindingKind + ':' + litLight.bindingRef, glow_radius_cm: 150 },
|
||||||
|
] };
|
||||||
|
c._regSignature = ''; c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete;
|
||||||
|
const rOwn = Number(sr().querySelector('.glowlayer circle')?.getAttribute('r'));
|
||||||
|
out.perSourceRadius = Math.abs(rOwn - c._cmToUnits(150)) < 0.5;
|
||||||
|
c._serverCfg = { ...c._serverCfg, markers: (c._serverCfg.markers || []).filter((m) => m.id !== litMarkerId) };
|
||||||
|
c._regSignature = ''; c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete;
|
||||||
// 7) радиус из настроек: 600 см против 300 см — вдвое больше
|
// 7) радиус из настроек: 600 см против 300 см — вдвое больше
|
||||||
const r600 = Number(sr().querySelector('.glowlayer circle')?.getAttribute('r'));
|
const r600 = Number(sr().querySelector('.glowlayer circle')?.getAttribute('r'));
|
||||||
c._serverCfg = { ...c._serverCfg, settings: { ...(c._serverCfg.settings || {}), glow_radius_cm: 300 } };
|
c._serverCfg = { ...c._serverCfg, settings: { ...(c._serverCfg.settings || {}), glow_radius_cm: 300 } };
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Vendored
+15
-5
File diff suppressed because one or more lines are too long
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.36.2 — 2026-07-23
|
||||||
|
- **Glow radius is now per source**: every device dialog gained a "Glow
|
||||||
|
radius" field (in your HA units; empty = the global default from general
|
||||||
|
settings, shown as the placeholder). A kettle's night light can glow half
|
||||||
|
a meter while the ceiling lamp floods the room. Door sectors use the same
|
||||||
|
per-source radius.
|
||||||
|
|
||||||
## v1.36.1 — 2026-07-23
|
## v1.36.1 — 2026-07-23
|
||||||
- Fixed tap-toggle "doing nothing" on lamps whose individual `light.*` entity
|
- Fixed tap-toggle "doing nothing" on lamps whose individual `light.*` entity
|
||||||
is **hidden in the registry** (the usual setup when lamps are folded into a
|
is **hidden in the registry** (the usual setup when lamps are folded into a
|
||||||
|
|||||||
@@ -140,6 +140,9 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
|||||||
(explicit ripple color still wins); off/white lights unchanged [auto]
|
(explicit ripple color still wins); off/white lights unchanged [auto]
|
||||||
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
|
- [ ] 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 [auto]; reduced-motion static
|
display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static
|
||||||
|
- [ ] Per-source glow radius (v1.36.2): the device dialog has a "Glow radius"
|
||||||
|
field (HA units; empty = general-settings default shown as placeholder);
|
||||||
|
an override changes that source's pool and door sectors only [auto]
|
||||||
- [ ] Hidden-light primary (v1.36.1): a lamp whose light entity is HIDDEN in
|
- [ ] Hidden-light primary (v1.36.1): a lamp whose light entity is HIDDEN in
|
||||||
the registry (folded into a light group) still toggles/reflects the lamp,
|
the registry (folded into a light group) still toggles/reflects the lamp,
|
||||||
not its do-not-disturb switch or identify button; visible entities of the
|
not its do-not-disturb switch or identify button; visible entities of the
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.36.1",
|
"version": "1.36.2",
|
||||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+35
-6
@@ -32,7 +32,7 @@ import './space-card';
|
|||||||
import { cardStyles } from './styles';
|
import { cardStyles } from './styles';
|
||||||
import { langOf, t, type I18nKey } from './i18n';
|
import { langOf, t, type I18nKey } from './i18n';
|
||||||
|
|
||||||
const CARD_VERSION = '1.36.1';
|
const CARD_VERSION = '1.36.2';
|
||||||
const LS_KEY = 'houseplan_card_layout_v1';
|
const LS_KEY = 'houseplan_card_layout_v1';
|
||||||
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||||
@@ -168,6 +168,7 @@ class HouseplanCard extends LitElement {
|
|||||||
tapAction: string; // '' = card default
|
tapAction: string; // '' = card default
|
||||||
controls: string[]; // entities this icon toggles as a group
|
controls: string[]; // entities this icon toggles as a group
|
||||||
controlsFilter: string;
|
controlsFilter: string;
|
||||||
|
glowRadius: string; // per-device glow radius in display units; '' = global default
|
||||||
model: string;
|
model: string;
|
||||||
link: string;
|
link: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -2007,6 +2008,11 @@ class HouseplanCard extends LitElement {
|
|||||||
tapAction: d.marker?.tap_action || '',
|
tapAction: d.marker?.tap_action || '',
|
||||||
controls: [...(d.marker?.controls || [])],
|
controls: [...(d.marker?.controls || [])],
|
||||||
controlsFilter: '',
|
controlsFilter: '',
|
||||||
|
glowRadius: Number(d.marker?.glow_radius_cm) > 0
|
||||||
|
? String(this._imperial
|
||||||
|
? Math.round((Number(d.marker!.glow_radius_cm) / 30.48) * 10) / 10
|
||||||
|
: Math.round(Number(d.marker!.glow_radius_cm)) / 100)
|
||||||
|
: '',
|
||||||
model: d.model || '',
|
model: d.model || '',
|
||||||
link: d.link || '',
|
link: d.link || '',
|
||||||
description: d.description || '',
|
description: d.description || '',
|
||||||
@@ -2020,7 +2026,7 @@ class HouseplanCard extends LitElement {
|
|||||||
this._markerDialog = {
|
this._markerDialog = {
|
||||||
name: '', binding: 'virtual', bindingFilter: '', icon: '', autoIcon: '',
|
name: '', binding: 'virtual', bindingFilter: '', icon: '', autoIcon: '',
|
||||||
display: 'badge', rippleColor: '', rippleSize: 3, size: 1, angle: 0,
|
display: 'badge', rippleColor: '', rippleSize: 3, size: 1, angle: 0,
|
||||||
tapAction: '', controls: [], controlsFilter: '', model: '',
|
tapAction: '', controls: [], controlsFilter: '', glowRadius: '', model: '',
|
||||||
link: '', description: '', pdfs: [], room: '', busy: false,
|
link: '', description: '', pdfs: [], room: '', busy: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2215,6 +2221,11 @@ class HouseplanCard extends LitElement {
|
|||||||
angle: dlg.angle ? dlg.angle : null,
|
angle: dlg.angle ? dlg.angle : null,
|
||||||
tap_action: dlg.tapAction || null,
|
tap_action: dlg.tapAction || null,
|
||||||
controls: dlg.controls.length ? dlg.controls : null,
|
controls: dlg.controls.length ? dlg.controls : null,
|
||||||
|
glow_radius_cm: (() => {
|
||||||
|
const v = parseFloat(dlg.glowRadius);
|
||||||
|
if (!Number.isFinite(v) || v <= 0) return null;
|
||||||
|
return Math.round(this._imperial ? v * 30.48 : v * 100);
|
||||||
|
})(),
|
||||||
model: dlg.model.trim() || null,
|
model: dlg.model.trim() || null,
|
||||||
link: dlg.link.trim() || null,
|
link: dlg.link.trim() || null,
|
||||||
description: dlg.description.trim() || null,
|
description: dlg.description.trim() || null,
|
||||||
@@ -2658,16 +2669,21 @@ class HouseplanCard extends LitElement {
|
|||||||
return this.hass?.config?.unit_system?.length === 'mi';
|
return this.hass?.config?.unit_system?.length === 'mi';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _glowRadiusPlaceholder(): string {
|
||||||
|
const cm = this._glowRadiusCm;
|
||||||
|
return this._imperial ? String(Math.round((cm / 30.48) * 10) / 10) : String(cm / 100);
|
||||||
|
}
|
||||||
|
|
||||||
/** Light pools of the current space: dark house, glowing sources. */
|
/** Light pools of the current space: dark house, glowing sources. */
|
||||||
private _renderGlowLayer(space: SpaceModel): TemplateResult {
|
private _renderGlowLayer(space: SpaceModel): TemplateResult {
|
||||||
const colors = this._fillColors;
|
const colors = this._fillColors;
|
||||||
const R = (this._glowRadiusCm / this._cellCm) * this._gridPitch;
|
const defaultR = (this._glowRadiusCm / this._cellCm) * this._gridPitch;
|
||||||
const g = this._gridPitch;
|
const g = this._gridPitch;
|
||||||
const polys = space.rooms
|
const polys = space.rooms
|
||||||
.map((r) => ({ r, poly: roomPoly(r) }))
|
.map((r) => ({ r, poly: roomPoly(r) }))
|
||||||
.filter((x): x is { r: RoomCfg; poly: number[][] } => !!x.poly);
|
.filter((x): x is { r: RoomCfg; poly: number[][] } => !!x.poly);
|
||||||
const doors = this._openingsR.filter((o) => o.type === 'door');
|
const doors = this._openingsR.filter((o) => o.type === 'door');
|
||||||
const spots: { pos: { x: number; y: number }; c: string; alpha: number; clip: string | null }[] = [];
|
const spots: { pos: { x: number; y: number }; c: string; alpha: number; clip: string | null; r: number }[] = [];
|
||||||
for (const d of this._devices) {
|
for (const d of this._devices) {
|
||||||
if (d.space !== space.id) continue;
|
if (d.space !== space.id) continue;
|
||||||
const lightEid = d.entities.find(
|
const lightEid = d.entities.find(
|
||||||
@@ -2676,6 +2692,9 @@ class HouseplanCard extends LitElement {
|
|||||||
if (!lightEid) continue;
|
if (!lightEid) continue;
|
||||||
const glow = glowColorOf(this.hass.states[lightEid], colors.glow_light.c);
|
const glow = glowColorOf(this.hass.states[lightEid], colors.glow_light.c);
|
||||||
if (!glow) continue;
|
if (!glow) continue;
|
||||||
|
// per-source radius (owner's decision v1.36.2): marker override, else global
|
||||||
|
const ownCm = Number(d.marker?.glow_radius_cm);
|
||||||
|
const R = Number.isFinite(ownCm) && ownCm > 0 ? (ownCm / this._cellCm) * this._gridPitch : defaultR;
|
||||||
const pos = this._pos(d);
|
const pos = this._pos(d);
|
||||||
// innermost room under the source (islands win — reverse order)
|
// innermost room under the source (islands win — reverse order)
|
||||||
const home = [...polys].reverse().find((x) => this._pointInRoom([pos.x, pos.y], x.r));
|
const home = [...polys].reverse().find((x) => this._pointInRoom([pos.x, pos.y], x.r));
|
||||||
@@ -2696,7 +2715,7 @@ class HouseplanCard extends LitElement {
|
|||||||
}
|
}
|
||||||
clip = shapes.join(' ');
|
clip = shapes.join(' ');
|
||||||
}
|
}
|
||||||
spots.push({ pos, c: glow.c, alpha: colors.glow_light.a * glow.bri, clip });
|
spots.push({ pos, c: glow.c, alpha: colors.glow_light.a * glow.bri, clip, r: R });
|
||||||
}
|
}
|
||||||
if (!spots.length) return svg`` as unknown as TemplateResult;
|
if (!spots.length) return svg`` as unknown as TemplateResult;
|
||||||
return svg`<defs>
|
return svg`<defs>
|
||||||
@@ -2709,7 +2728,7 @@ class HouseplanCard extends LitElement {
|
|||||||
${sp.clip ? svg`<clipPath id="hp-glowclip-${i}"><path d="${sp.clip}"></path></clipPath>` : nothing}`)}
|
${sp.clip ? svg`<clipPath id="hp-glowclip-${i}"><path d="${sp.clip}"></path></clipPath>` : nothing}`)}
|
||||||
</defs>
|
</defs>
|
||||||
<g class="glowlayer">
|
<g class="glowlayer">
|
||||||
${spots.map((sp, i) => svg`<circle cx="${sp.pos.x}" cy="${sp.pos.y}" r="${R}"
|
${spots.map((sp, i) => svg`<circle cx="${sp.pos.x}" cy="${sp.pos.y}" r="${sp.r}"
|
||||||
fill="url(#hp-glow-${i})" ${''}
|
fill="url(#hp-glow-${i})" ${''}
|
||||||
clip-path=${sp.clip ? `url(#hp-glowclip-${i})` : nothing}></circle>`)}
|
clip-path=${sp.clip ? `url(#hp-glowclip-${i})` : nothing}></circle>`)}
|
||||||
</g>` as unknown as TemplateResult;
|
</g>` as unknown as TemplateResult;
|
||||||
@@ -3829,6 +3848,16 @@ class HouseplanCard extends LitElement {
|
|||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
|
<label>${this._t('marker.glow_radius_label')}</label>
|
||||||
|
<div class="colorrow">
|
||||||
|
<input class="tempin" type="number" min="0.5" step="0.5"
|
||||||
|
placeholder=${this._glowRadiusPlaceholder}
|
||||||
|
.value=${d.glowRadius}
|
||||||
|
@input=${(e: Event) => (this._markerDialog = { ...d, glowRadius: (e.target as HTMLInputElement).value })} />
|
||||||
|
<span class="opl">${this._imperial ? this._t('gs.unit_ft') : this._t('gs.unit_m')}</span>
|
||||||
|
<span class="opl muted">${this._t('marker.glow_radius_hint')}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>${this._t('marker.icon_label')}</label>
|
<label>${this._t('marker.icon_label')}</label>
|
||||||
${customElements.get('ha-icon-picker')
|
${customElements.get('ha-icon-picker')
|
||||||
? html`<ha-icon-picker .hass=${this.hass} .value=${d.icon}
|
? html`<ha-icon-picker .hass=${this.hass} .value=${d.icon}
|
||||||
|
|||||||
+3
-1
@@ -291,5 +291,7 @@
|
|||||||
"marker.controls_label": "Controls light sources",
|
"marker.controls_label": "Controls light sources",
|
||||||
"marker.controls_hint": "With tap action “Toggle”, a click flips all bound lights at once (any on → all off). The icon mirrors their state.",
|
"marker.controls_hint": "With tap action “Toggle”, a click flips all bound lights at once (any on → all off). The icon mirrors their state.",
|
||||||
"marker.controls_filter": "Search lights and switches…",
|
"marker.controls_filter": "Search lights and switches…",
|
||||||
"info.controls": "Controls"
|
"info.controls": "Controls",
|
||||||
|
"marker.glow_radius_label": "Glow radius (light-sources fill)",
|
||||||
|
"marker.glow_radius_hint": "empty = default from general settings"
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -291,5 +291,7 @@
|
|||||||
"marker.controls_label": "Управляет источниками света",
|
"marker.controls_label": "Управляет источниками света",
|
||||||
"marker.controls_hint": "При действии «Переключить» клик разом переключает все привязанные источники (горит хоть один → выключить все). Значок отражает их состояние.",
|
"marker.controls_hint": "При действии «Переключить» клик разом переключает все привязанные источники (горит хоть один → выключить все). Значок отражает их состояние.",
|
||||||
"marker.controls_filter": "Поиск ламп и выключателей…",
|
"marker.controls_filter": "Поиск ламп и выключателей…",
|
||||||
"info.controls": "Управляет"
|
"info.controls": "Управляет",
|
||||||
|
"marker.glow_radius_label": "Радиус свечения (заливка «Свет по источникам»)",
|
||||||
|
"marker.glow_radius_hint": "пусто = по умолчанию из общих настроек"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export interface Marker {
|
|||||||
angle?: number | null; // icon rotation, degrees
|
angle?: number | null; // icon rotation, degrees
|
||||||
/** Entities this icon toggles as a group (wall switch → its lights). */
|
/** Entities this icon toggles as a group (wall switch → its lights). */
|
||||||
controls?: string[] | null;
|
controls?: string[] | null;
|
||||||
|
/** Per-source glow radius in cm (glow fill); null = the global default. */
|
||||||
|
glow_radius_cm?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A door or window: plan geometry (normalized coords), optionally live via entities. */
|
/** A door or window: plan geometry (normalized coords), optionally live via entities. */
|
||||||
|
|||||||
Reference in New Issue
Block a user