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:
Matysh
2026-07-27 12:41:26 +03:00
parent a841d17543
commit e04ef2f2e6
16 changed files with 442 additions and 83 deletions
+44
View File
@@ -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);
+16
View File
@@ -93,6 +93,22 @@ const res = await page.evaluate(async () => {
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;
// 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 см — вдвое больше
const r600 = Number(sr().querySelector('.glowlayer circle')?.getAttribute('r'));
c._serverCfg = { ...c._serverCfg, settings: { ...(c._serverCfg.settings || {}), glow_radius_cm: 300 } };
File diff suppressed because one or more lines are too long