mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
feat v1.44.0: control-first device card + light-source flag (user feedback)
- device card opens with controllable entities: toggles inline (finger targets), cover/lock/climate hand off to HA more-info; metadata and manuals moved below; config/diagnostic entities filtered; locks still never toggle from a card - marker.is_light: a smart switch driving dumb fixtures glows in the light-sources fill (its own entity or the bound controls) — no light-group helper needed - backend schema; smoke_card_controls.mjs, smoke_glow extended; docs same-commit
This commit is contained in:
@@ -13,7 +13,7 @@ FILES_URL = "/houseplan_files/files"
|
|||||||
CONTENT_URL = "/api/houseplan/content"
|
CONTENT_URL = "/api/houseplan/content"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.43.3"
|
VERSION = "1.44.0"
|
||||||
|
|
||||||
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.43.3"
|
"version": "1.44.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ MARKER_SCHEMA = vol.Schema(
|
|||||||
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("glow_radius_cm"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=10, max=10000)), None),
|
||||||
|
vol.Optional("is_light"): vol.Any(bool, 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),
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
|
const { page, browser } = await launch();
|
||||||
|
const res = await page.evaluate(async () => {
|
||||||
|
const out = {};
|
||||||
|
const c = window.__card;
|
||||||
|
const sr = () => c.shadowRoot || c.renderRoot;
|
||||||
|
const calls = [];
|
||||||
|
c.hass = { ...c.hass, callService: (d, s, data) => { calls.push([d, s, data.entity_id]); return Promise.resolve(); } };
|
||||||
|
await c.updateComplete;
|
||||||
|
c._setMode('view'); await c.updateComplete;
|
||||||
|
// устройство с управляемой сущностью
|
||||||
|
const dev = c._devices.find((d) => d.entities?.some((e) => e.startsWith('light.') || e.startsWith('switch.')));
|
||||||
|
out.hasDev = !!dev;
|
||||||
|
c._infoCard = dev; await c.updateComplete;
|
||||||
|
// 1) блок сущностей идёт ПЕРВЫМ, до модели/ссылок
|
||||||
|
const body = sr().querySelector('.dialog .body');
|
||||||
|
out.entListFirst = body.firstElementChild?.classList.contains('entlist')
|
||||||
|
|| body.querySelector('.entlist') === body.children[0];
|
||||||
|
const rows = [...sr().querySelectorAll('.entrow')];
|
||||||
|
out.rows = rows.length > 0;
|
||||||
|
// 2) у переключаемой сущности — кнопка с крупной зоной нажатия
|
||||||
|
const btn = sr().querySelector('.entbtn');
|
||||||
|
out.hasButton = !!btn;
|
||||||
|
const box = btn?.getBoundingClientRect();
|
||||||
|
out.tapTarget = box ? box.height >= 30 && box.width >= 60 : null;
|
||||||
|
// 3) кнопка реально переключает
|
||||||
|
const before = calls.length;
|
||||||
|
btn.click(); await c.updateComplete;
|
||||||
|
out.toggles = calls.length > before && calls.at(-1)[1] === 'toggle';
|
||||||
|
// 4) замок никогда не переключается из карточки
|
||||||
|
const n = calls.length;
|
||||||
|
c._cardToggle('lock.front_door');
|
||||||
|
out.lockNeverToggles = calls.length === n;
|
||||||
|
// 5) диагностические/конфиг-сущности не засоряют список
|
||||||
|
const ents = c._cardEntities(dev).map((e) => e.eid);
|
||||||
|
out.noConfigEntities = ents.every((e) => {
|
||||||
|
const cat = c.hass.entities[e]?.entity_category;
|
||||||
|
return cat !== 'config' && cat !== 'diagnostic';
|
||||||
|
});
|
||||||
|
c._infoCard = null; await c.updateComplete;
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
checkAll(res);
|
||||||
|
await finish(browser, res);
|
||||||
@@ -93,6 +93,22 @@ const res = await page.evaluate(async () => {
|
|||||||
out.perSourceRadius = Math.abs(rOwn - c._cmToUnits(150)) < 0.5;
|
out.perSourceRadius = Math.abs(rOwn - c._cmToUnits(150)) < 0.5;
|
||||||
c._serverCfg = { ...c._serverCfg, markers: (c._serverCfg.markers || []).filter((m) => m.id !== litMarkerId) };
|
c._serverCfg = { ...c._serverCfg, markers: (c._serverCfg.markers || []).filter((m) => m.id !== litMarkerId) };
|
||||||
c._regSignature = ''; c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete;
|
c._regSignature = ''; c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete;
|
||||||
|
// 6в) флаг «источник света»: умный выключатель с обычными светильниками
|
||||||
|
const swDev = c._devices.find((d) => d.space === spId && d.entities.some((e) => e.startsWith('switch.')));
|
||||||
|
if (swDev) {
|
||||||
|
const swEid = swDev.entities.find((e) => e.startsWith('switch.'));
|
||||||
|
c.hass = { ...c.hass, states: { ...c.hass.states, [swEid]: { ...c.hass.states[swEid], state: 'on' } } };
|
||||||
|
const spotsBefore = sr().querySelectorAll('.glowlayer circle').length;
|
||||||
|
c._serverCfg = { ...c._serverCfg, markers: [
|
||||||
|
...(c._serverCfg.markers || []).filter((m) => m.id !== swDev.id),
|
||||||
|
{ id: swDev.id, binding: swDev.bindingKind + ':' + swDev.bindingRef, is_light: true },
|
||||||
|
] };
|
||||||
|
c._regSignature = ''; c._maybeRebuildDevices(); c._saveConfig(); c.requestUpdate(); await c.updateComplete;
|
||||||
|
out.switchGlows = sr().querySelectorAll('.glowlayer circle').length === spotsBefore + 1;
|
||||||
|
c._serverCfg = { ...c._serverCfg, markers: (c._serverCfg.markers || []).filter((m) => m.id !== swDev.id) };
|
||||||
|
c._regSignature = ''; c._maybeRebuildDevices(); c._saveConfig(); c.requestUpdate(); await c.updateComplete;
|
||||||
|
out.switchGlowsOffByDefault = sr().querySelectorAll('.glowlayer circle').length === spotsBefore;
|
||||||
|
} else { out.switchGlows = 'no-switch'; out.switchGlowsOffByDefault = 'no-switch'; }
|
||||||
// 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
+74
-24
File diff suppressed because one or more lines are too long
@@ -1,5 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.44.0 — 2026-07-27 (user feedback: control first)
|
||||||
|
|
||||||
|
- **The device card is now a control surface.** It opens with the device's
|
||||||
|
controllable entities: lights, switches and fans toggle straight from the
|
||||||
|
card with finger-sized buttons, covers/locks/climate open Home Assistant's
|
||||||
|
own more-info. Model, links and PDF manuals moved below — on a wall tablet
|
||||||
|
this card is for running the home, not for reading documentation (field
|
||||||
|
report). Config and diagnostic entities are not listed; locks still never
|
||||||
|
toggle from a card tap.
|
||||||
|
- **"This device is a light source"** — a new per-device flag. A smart switch
|
||||||
|
driving ordinary (dumb) fixtures now casts a glow in the "Light sources"
|
||||||
|
fill without inventing a light-group helper: the glow follows the switch, or
|
||||||
|
the lights bound under "Controls light sources" when they are set.
|
||||||
|
|
||||||
## v1.43.3 — 2026-07-27 (user feedback: discoverability and touch)
|
## v1.43.3 — 2026-07-27 (user feedback: discoverability and touch)
|
||||||
|
|
||||||
- **Room settings were unfindable.** The gear added in v1.42.0 lived inside the
|
- **Room settings were unfindable.** The gear added in v1.42.0 lived inside the
|
||||||
|
|||||||
@@ -30,6 +30,16 @@
|
|||||||
ANY touch/pen pointer event, even if the browser claims `hover: hover`
|
ANY touch/pen pointer event, even if the browser claims `hover: hover`
|
||||||
(stylus, paired mouse, vendor skins) [auto: smoke_feedback_v2]
|
(stylus, paired mouse, vendor skins) [auto: smoke_feedback_v2]
|
||||||
|
|
||||||
|
- [ ] Light-source flag (v1.44.0, user feedback): a smart SWITCH driving dumb
|
||||||
|
fixtures glows in the "Light sources" fill once "This device is a light
|
||||||
|
source" is ticked (its own entity or the lights bound under "Controls");
|
||||||
|
unticked devices without a light entity never glow [auto: smoke_glow]
|
||||||
|
- [ ] Device card controls (v1.44.0): the device card opens with its
|
||||||
|
controllable entities FIRST — toggles right there (≥30 px tap targets),
|
||||||
|
cover/lock/climate open HA more-info; model, links and manuals moved
|
||||||
|
below; config/diagnostic entities are not listed; locks never toggle from
|
||||||
|
the card [auto: smoke_card_controls]
|
||||||
|
|
||||||
## Environments matrix
|
## Environments matrix
|
||||||
|
|
||||||
Run the *core flows* (marked ★ below) in each environment at least once per minor release:
|
Run the *core flows* (marked ★ below) in each environment at least once per minor release:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.43.3",
|
"version": "1.44.0",
|
||||||
"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",
|
||||||
|
|||||||
+84
-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.43.3';
|
const CARD_VERSION = '1.44.0';
|
||||||
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';
|
||||||
@@ -233,6 +233,7 @@ class HouseplanCard extends LitElement {
|
|||||||
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
|
glowRadius: string; // per-device glow radius in display units; '' = global default
|
||||||
|
isLight: boolean; // force this marker to glow (dumb fixtures behind a switch)
|
||||||
model: string;
|
model: string;
|
||||||
link: string;
|
link: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -2429,6 +2430,7 @@ class HouseplanCard extends LitElement {
|
|||||||
defaultTap: d.primary?.split('.')[0] === 'light' ? 'toggle' : 'info',
|
defaultTap: d.primary?.split('.')[0] === 'light' ? 'toggle' : 'info',
|
||||||
controls: [...(d.marker?.controls || [])],
|
controls: [...(d.marker?.controls || [])],
|
||||||
controlsFilter: '',
|
controlsFilter: '',
|
||||||
|
isLight: d.marker?.is_light === true,
|
||||||
glowRadius: Number(d.marker?.glow_radius_cm) > 0
|
glowRadius: Number(d.marker?.glow_radius_cm) > 0
|
||||||
? String(this._imperial
|
? String(this._imperial
|
||||||
? Math.round((Number(d.marker!.glow_radius_cm) / 30.48) * 10) / 10
|
? Math.round((Number(d.marker!.glow_radius_cm) / 30.48) * 10) / 10
|
||||||
@@ -2448,7 +2450,8 @@ class HouseplanCard extends LitElement {
|
|||||||
name: '', binding: 'virtual', bindingMode: 'virtual', bindingOpen: false,
|
name: '', binding: 'virtual', bindingMode: 'virtual', bindingOpen: false,
|
||||||
showEntities: false, bindingFilter: '', icon: '', autoIcon: '',
|
showEntities: false, bindingFilter: '', icon: '', autoIcon: '',
|
||||||
display: 'badge', rippleColor: '', rippleSize: 3, size: 1, angle: 0,
|
display: 'badge', rippleColor: '', rippleSize: 3, size: 1, angle: 0,
|
||||||
tapAction: '', defaultTap: 'info', controls: [], controlsFilter: '', glowRadius: '', model: '',
|
tapAction: '', defaultTap: 'info', controls: [], controlsFilter: '', isLight: false,
|
||||||
|
glowRadius: '', model: '',
|
||||||
link: '', description: '', pdfs: [], room: '', busy: false,
|
link: '', description: '', pdfs: [], room: '', busy: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2642,6 +2645,7 @@ class HouseplanCard extends LitElement {
|
|||||||
tap_action: dlg.tapAction || null,
|
tap_action: dlg.tapAction || null,
|
||||||
controls: dlg.controls.length ? dlg.controls : null,
|
controls: dlg.controls.length ? dlg.controls : null,
|
||||||
// pdfs may be rewritten below when rebinding changes the marker id
|
// pdfs may be rewritten below when rebinding changes the marker id
|
||||||
|
is_light: dlg.isLight ? true : null,
|
||||||
glow_radius_cm: (() => {
|
glow_radius_cm: (() => {
|
||||||
const v = parseFloat(dlg.glowRadius);
|
const v = parseFloat(dlg.glowRadius);
|
||||||
if (!Number.isFinite(v) || v <= 0) return null;
|
if (!Number.isFinite(v) || v <= 0) return null;
|
||||||
@@ -3133,9 +3137,15 @@ class HouseplanCard extends LitElement {
|
|||||||
const spots: { pos: { x: number; y: number }; c: string; alpha: number; clip: string[] | null; r: number }[] = [];
|
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(
|
// A light source is normally a device with a lit light.* entity. With the
|
||||||
(e) => e.startsWith('light.') && this.hass.states[e]?.state === 'on',
|
// "is a light source" flag (field request: a smart SWITCH driving dumb
|
||||||
);
|
// fixtures) any lit entity counts — the switch itself, or the lights it
|
||||||
|
// controls when they are bound.
|
||||||
|
const forced = d.marker?.is_light === true;
|
||||||
|
const pool = forced
|
||||||
|
? [...(d.marker?.controls || []), ...d.entities]
|
||||||
|
: d.entities.filter((e) => e.startsWith('light.'));
|
||||||
|
const lightEid = pool.find((e) => this.hass.states[e]?.state === 'on');
|
||||||
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;
|
||||||
@@ -4485,6 +4495,39 @@ class HouseplanCard extends LitElement {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Entities of a device worth CONTROLLING or reading, in a sensible order. */
|
||||||
|
private _cardEntities(d: DevItem): { eid: string; kind: 'toggle' | 'value' | 'open' }[] {
|
||||||
|
const h = this.hass;
|
||||||
|
const out: { eid: string; kind: 'toggle' | 'value' | 'open' }[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const push = (eid: string) => {
|
||||||
|
if (!eid || seen.has(eid) || !h.states[eid]) return;
|
||||||
|
const reg = h.entities[eid];
|
||||||
|
if (reg?.entity_category === 'config' || reg?.entity_category === 'diagnostic') return;
|
||||||
|
seen.add(eid);
|
||||||
|
const dom = eid.split('.')[0];
|
||||||
|
if (['light', 'switch', 'fan', 'humidifier', 'siren', 'input_boolean'].includes(dom))
|
||||||
|
out.push({ eid, kind: 'toggle' });
|
||||||
|
else if (['cover', 'valve', 'lock', 'climate', 'media_player', 'vacuum', 'water_heater'].includes(dom))
|
||||||
|
out.push({ eid, kind: 'open' }); // needs the full more-info UI
|
||||||
|
else if (['sensor', 'binary_sensor', 'number', 'select'].includes(dom))
|
||||||
|
out.push({ eid, kind: 'value' });
|
||||||
|
};
|
||||||
|
for (const e of d.marker?.controls || []) push(e);
|
||||||
|
if (d.primary) push(d.primary);
|
||||||
|
for (const e of d.entities) push(e);
|
||||||
|
return out.slice(0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggle straight from the device card (safe domains only). */
|
||||||
|
private _cardToggle(eid: string): void {
|
||||||
|
const dom = eid.split('.')[0];
|
||||||
|
if (dom === 'lock' || dom === 'alarm_control_panel') return; // never from a card tap
|
||||||
|
this.hass
|
||||||
|
.callService('homeassistant', 'toggle', { entity_id: eid })
|
||||||
|
.catch((e: any) => this._showToast(this._t('toast.error', { err: this._errText(e) })));
|
||||||
|
}
|
||||||
|
|
||||||
private _renderInfoCard(): TemplateResult {
|
private _renderInfoCard(): TemplateResult {
|
||||||
const d = this._infoCard!;
|
const d = this._infoCard!;
|
||||||
const st = d.primary ? this.hass.states[d.primary] : undefined;
|
const st = d.primary ? this.hass.states[d.primary] : undefined;
|
||||||
@@ -4494,8 +4537,38 @@ class HouseplanCard extends LitElement {
|
|||||||
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
|
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
|
||||||
<div class="hd"><ha-icon icon="${d.icon}"></ha-icon>${d.name}</div>
|
<div class="hd"><ha-icon icon="${d.icon}"></ha-icon>${d.name}</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
|
${(() => {
|
||||||
|
// Field feedback: on a wall tablet this card is for CONTROLLING the
|
||||||
|
// home; model/links/manuals are reference material and belong below.
|
||||||
|
const ents = this._cardEntities(d);
|
||||||
|
if (!ents.length) return nothing;
|
||||||
|
return html`<div class="entlist">
|
||||||
|
${ents.map(({ eid, kind }) => {
|
||||||
|
const est = this.hass.states[eid];
|
||||||
|
const name = this.hass.entities[eid]?.name
|
||||||
|
|| est?.attributes?.friendly_name || eid;
|
||||||
|
const val = est ? this.hass.formatEntityState?.(est) ?? est.state : '';
|
||||||
|
const on = est?.state === 'on' || ['open', 'unlocked', 'playing', 'cleaning'].includes(est?.state);
|
||||||
|
return html`<div class="entrow ${on ? 'on' : ''}">
|
||||||
|
<ha-icon icon=${stateIcon(
|
||||||
|
iconFor(name, '', this._iconRules), eid.split('.')[0],
|
||||||
|
est?.attributes?.device_class, est?.state, false,
|
||||||
|
)}></ha-icon>
|
||||||
|
<span class="en">${name}</span>
|
||||||
|
${kind === 'toggle'
|
||||||
|
? html`<button class="entbtn ${on ? 'on' : ''}"
|
||||||
|
@click=${() => this._cardToggle(eid)}>${val}</button>`
|
||||||
|
: kind === 'open'
|
||||||
|
? html`<button class="entbtn"
|
||||||
|
@click=${() => { this._infoCard = null; this._openMoreInfo(eid); }}>${val}</button>`
|
||||||
|
: html`<span class="ev">${val}</span>`}
|
||||||
|
</div>`;
|
||||||
|
})}
|
||||||
|
</div>`;
|
||||||
|
})()}
|
||||||
${d.model ? html`<div class="inforow"><span class="k">${this._t('info.model')}</span><span>${d.model}</span></div>` : nothing}
|
${d.model ? html`<div class="inforow"><span class="k">${this._t('info.model')}</span><span>${d.model}</span></div>` : nothing}
|
||||||
${stateTxt ? html`<div class="inforow"><span class="k">${this._t('info.state')}</span><span>${stateTxt}</span></div>` : nothing}
|
${stateTxt && !this._cardEntities(d).length
|
||||||
|
? html`<div class="inforow"><span class="k">${this._t('info.state')}</span><span>${stateTxt}</span></div>` : nothing}
|
||||||
${safeUrl(d.link)
|
${safeUrl(d.link)
|
||||||
? html`<div class="inforow"><span class="k">${this._t('info.link')}</span>
|
? html`<div class="inforow"><span class="k">${this._t('info.link')}</span>
|
||||||
<a href="${safeUrl(d.link)}" target="_blank" rel="noreferrer noopener">${d.link}</a></div>`
|
<a href="${safeUrl(d.link)}" target="_blank" rel="noreferrer noopener">${d.link}</a></div>`
|
||||||
@@ -4663,6 +4736,11 @@ class HouseplanCard extends LitElement {
|
|||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
|
<label class="srcrow" title=${this._t('marker.is_light_tip')}>
|
||||||
|
<input type="checkbox" .checked=${d.isLight}
|
||||||
|
@change=${(e: Event) => (this._markerDialog = { ...d, isLight: (e.target as HTMLInputElement).checked })} />
|
||||||
|
<span>${this._t('marker.is_light')}</span>
|
||||||
|
</label>
|
||||||
<label>${this._t('marker.glow_radius_label')}</label>
|
<label>${this._t('marker.glow_radius_label')}</label>
|
||||||
<div class="colorrow">
|
<div class="colorrow">
|
||||||
<input class="tempin" type="number" min="0.5" step="0.5"
|
<input class="tempin" type="number" min="0.5" step="0.5"
|
||||||
|
|||||||
+3
-1
@@ -325,5 +325,7 @@
|
|||||||
"preview.room_name": "Living room",
|
"preview.room_name": "Living room",
|
||||||
"toast.cfg_reload_failed": "Could not reload the plan from the server: {err}",
|
"toast.cfg_reload_failed": "Could not reload the plan from the server: {err}",
|
||||||
"room.settings_short": "Room",
|
"room.settings_short": "Room",
|
||||||
"room.unnamed": "Unnamed room"
|
"room.unnamed": "Unnamed room",
|
||||||
|
"marker.is_light": "This device is a light source",
|
||||||
|
"marker.is_light_tip": "Makes the icon glow in the “Light sources” fill even without a light entity — for a smart switch driving ordinary fixtures. The glow follows the switch (or the lights bound above)."
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -325,5 +325,7 @@
|
|||||||
"preview.room_name": "Гостиная",
|
"preview.room_name": "Гостиная",
|
||||||
"toast.cfg_reload_failed": "Не удалось перечитать план с сервера: {err}",
|
"toast.cfg_reload_failed": "Не удалось перечитать план с сервера: {err}",
|
||||||
"room.settings_short": "Комната",
|
"room.settings_short": "Комната",
|
||||||
"room.unnamed": "Комната без имени"
|
"room.unnamed": "Комната без имени",
|
||||||
|
"marker.is_light": "Это устройство — источник света",
|
||||||
|
"marker.is_light_tip": "Даёт ореол в заливке «Свет по источникам» даже без light-сущности — для умного выключателя с обычными светильниками. Ореол следует за выключателем (или за привязанными выше лампами)."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1215,6 +1215,42 @@ export const cardStyles = css`
|
|||||||
.pdftag .x:hover {
|
.pdftag .x:hover {
|
||||||
color: #ff7a5c;
|
color: #ff7a5c;
|
||||||
}
|
}
|
||||||
|
.entlist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.entrow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--secondary-background-color, rgba(128, 128, 128, 0.12));
|
||||||
|
}
|
||||||
|
.entrow ha-icon { --mdc-icon-size: 20px; color: var(--hp-muted); }
|
||||||
|
.entrow.on ha-icon { color: var(--hp-accent); }
|
||||||
|
.entrow .en { flex: 1; font-size: 13px; }
|
||||||
|
.entrow .ev { font-size: 13px; color: var(--hp-muted); }
|
||||||
|
.entbtn {
|
||||||
|
min-width: 74px;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: 1px solid var(--hp-muted);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--hp-txt);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.entbtn.on {
|
||||||
|
background: var(--hp-accent);
|
||||||
|
border-color: var(--hp-accent);
|
||||||
|
color: var(--text-primary-color, #fff);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.inforow {
|
.inforow {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ export interface Marker {
|
|||||||
controls?: string[] | null;
|
controls?: string[] | null;
|
||||||
/** Per-source glow radius in cm (glow fill); null = the global default. */
|
/** Per-source glow radius in cm (glow fill); null = the global default. */
|
||||||
glow_radius_cm?: number | null;
|
glow_radius_cm?: number | null;
|
||||||
|
/**
|
||||||
|
* Treat this marker as a light source in the glow fill even when it has no
|
||||||
|
* light.* entity (a smart switch driving dumb fixtures — field request).
|
||||||
|
* null/undefined = auto: any light.* entity of the device.
|
||||||
|
*/
|
||||||
|
is_light?: boolean | 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