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 spId = c._space; // включить glow-режим c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s : ({ ...s, settings: { ...(s.settings || {}), fill_mode: 'glow' } })) }; c.requestUpdate(); await c.updateComplete; await new Promise((r) => setTimeout(r, 250)); // 1) все комнаты залиты темнотой одинаково (независимо от ламп) const rooms = [...sr().querySelectorAll('.room.styled')]; out.roomsDark = rooms.length > 0 && rooms.every((el) => { const st = el.getAttribute('style') || ''; return st.includes('--room-fill:#0d1b2a'); }); // 2) пятна от включённых ламп const litLight = c._devices.find((d) => d.space === spId && d.entities.some((e) => e.startsWith('light.') && c.hass.states[e]?.state === 'on')); out.hasLitLight = !!litLight; out.spots = sr().querySelectorAll('.glowlayer circle').length; out.spotsMatchLights = out.spots === c._devices.filter((d) => d.space === spId && d.entities.some((e) => e.startsWith('light.') && c.hass.states[e]?.state === 'on')).length; // 3) выключение лампы убирает её пятно const eid = litLight.entities.find((e) => e.startsWith('light.') && c.hass.states[e]?.state === 'on'); const st0 = c.hass.states[eid]; c.hass = { ...c.hass, states: { ...c.hass.states, [eid]: { ...st0, state: 'off' } } }; await c.updateComplete; out.spotGone = sr().querySelectorAll('.glowlayer circle').length === out.spots - 1; c.hass = { ...c.hass, states: { ...c.hass.states, [eid]: st0 } }; await c.updateComplete; // 4) rgb-лампа красит градиент своим цветом (форсируем rgb у лампы) c.hass = { ...c.hass, states: { ...c.hass.states, [eid]: { ...st0, state: 'on', attributes: { ...st0.attributes, rgb_color: [255, 0, 0] } } } }; await c.updateComplete; out.gradientColored = [...sr().querySelectorAll('defs radialGradient stop')].some((s2) => s2.getAttribute('stop-color') === 'rgb(255, 0, 0)'); c.hass = { ...c.hass, states: { ...c.hass.states, [eid]: st0 } }; await c.updateComplete; // 5) пятно обрезано комнатой (есть clipPath) out.clipped = sr().querySelectorAll('defs clipPath[id^="hp-glowclip"]').length > 0; // 6) дверь в соседнюю комнату → в clip добавлен сектор (2 сабпути M) // добавим дверь между r1 и r2 на общей стене x=0.55… найдём общую стену программно: const spm = c._spaceModel(); const r1 = spm.rooms.find((r) => r.id === 'r1'); const r2 = spm.rooms.find((r) => r.id === 'r2'); // возьмём точку на границе r1, ближайшую к центру r2 const c2 = c._roomCenter(r2); const poly1 = r1.poly || [[r1.x, r1.y], [r1.x + r1.w, r1.y], [r1.x + r1.w, r1.y + r1.h], [r1.x, r1.y + r1.h]]; // общая стена вертикальная — дверь ставим на неё const H = 1000; // square canvas const doorPt = (() => { let best = null, bd = 1e9; for (const [x, y] of [[550, 150], [550, 200], [550, 250]]) { const d2 = Math.hypot(x - c2[0], y - c2[1]); if (d2 < bd) { bd = d2; best = [x, y]; } } return best; })(); c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s : ({ ...s, openings: [{ id: 'gd', type: 'door', x: doorPt[0] / 1000, y: doorPt[1] / H, angle: 90, length: 0.09 }] })) }; c.requestUpdate(); await c.updateComplete; // источник детерминированно ставим в центр r1 (двигаем реальную включённую лампу) const c1 = c._roomCenter(r1); c._layout = { ...c._layout, [litLight.id]: { s: spId, x: c1[0] / 1000, y: c1[1] / 1000 } }; // радиус 6 м, чтобы дверь заведомо была в зоне досягаемости c._serverCfg = { ...c._serverCfg, settings: { ...(c._serverCfg.settings || {}), glow_radius_cm: 600 } }; c.requestUpdate(); await c.updateComplete; // теперь каждый под-контур — отдельный path внутри clipPath const clipEls = [...sr().querySelectorAll('defs clipPath[id^="hp-glowclip"]')]; out.sectorAdded = clipEls.some((cp) => cp.querySelectorAll('path').length >= 2); // и сектор в том же направлении обхода не выедает комнату: каждый path — один контур out.singleSubpathPerPath = clipEls.every((cp) => [...cp.querySelectorAll('path')] .every((p) => ((p.getAttribute('d') || '').match(/M /g) || []).length === 1)); // у входной двери (наружу) сектора нет: дверь на внешней стене r1 (x=min) const minX = Math.min(...poly1.map((p) => p[0])); const yMid = (Math.min(...poly1.map((p) => p[1])) + Math.max(...poly1.map((p) => p[1]))) / 2; c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s : ({ ...s, openings: [{ id: 'gd2', type: 'door', x: minX / 1000, y: yMid / 1000, angle: 90, length: 0.09 }] })) }; c.requestUpdate(); await c.updateComplete; const clipEls2 = [...sr().querySelectorAll('defs clipPath[id^="hp-glowclip"]')]; out.entranceNoSector = clipEls2.every((cp) => cp.querySelectorAll('path').length === 1); // 6б) профиль градиента: плато до 80%, спад на внешних 20% const stops = [...sr().querySelectorAll('defs radialGradient')][0]; const offs = stops ? [...stops.querySelectorAll('stop')].map((st2) => [st2.getAttribute('offset'), st2.getAttribute('stop-opacity')]) : []; out.plateau80 = offs.length === 3 && offs[0][0] === '0%' && offs[1][0] === '70%' && offs[0][1] === offs[1][1] && offs[2][1] === '0'; // 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; // 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 } }; c.requestUpdate(); await c.updateComplete; const r300 = Number(sr().querySelector('.glowlayer circle')?.getAttribute('r')); out.radiusReacts = Math.abs(r600 / r300 - 2) < 0.01; return out; }); // значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) checkAll(res, { "spots": 1, }); await finish(browser, res);