mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
118 lines
5.4 KiB
JavaScript
118 lines
5.4 KiB
JavaScript
// Единый принцип жёлтого (2026-07-29): жёлтый = устройство прямо сейчас
|
||
// выполняет основную функцию. Термоголовка желтеет от реального нагрева
|
||
// (hvac_action), а не от служебного свитча защиты от накипи; горящая лампа
|
||
// желтит значок в любом режиме заливки тем же условием, что зажигает пятно.
|
||
import { launch, checkAll, finish } from './serve.mjs';
|
||
const { page, browser } = await launch();
|
||
const out = await page.evaluate(() => {
|
||
const o = {};
|
||
const c = window.__card;
|
||
const cls = (d, states) => {
|
||
const saved = c.hass;
|
||
const visibleSnapshot = c._visibleDeviceSnapshot;
|
||
const candidateSnapshot = c._candidateDeviceSnapshot;
|
||
const entities = Object.fromEntries(Object.keys(states).map((entity_id) => [
|
||
entity_id,
|
||
{ entity_id, platform: 'demo', disabled_by: null },
|
||
]));
|
||
c.hass = {
|
||
...c.hass,
|
||
entities: { ...c.hass.entities, ...entities },
|
||
states: { ...c.hass.states, ...states },
|
||
};
|
||
// Synthetic devices are deliberately outside the acquired demo registry.
|
||
// Exercise the resolver against the injected HA shape, not the previously
|
||
// committed plan frame used by the real DOM.
|
||
c._visibleDeviceSnapshot = null;
|
||
c._candidateDeviceSnapshot = null;
|
||
const r = c._stateClass(d);
|
||
c.hass = saved;
|
||
c._visibleDeviceSnapshot = visibleSnapshot;
|
||
c._candidateDeviceSnapshot = candidateSnapshot;
|
||
return r;
|
||
};
|
||
const trv = { id: 't', primary: 'climate.trv', entities: ['climate.trv', 'switch.trv_anti_scaling'], marker: null };
|
||
|
||
// реально греет → жёлтая
|
||
o.heatingIsYellow = cls(trv, {
|
||
'climate.trv': { state: 'heat', attributes: { hvac_action: 'heating' } },
|
||
'switch.trv_anti_scaling': { state: 'on' },
|
||
}) === 'on';
|
||
|
||
// включена, но не греет (idle) → чёрная, даже со включённой защитой от накипи
|
||
o.idleIsDark = cls(trv, {
|
||
'climate.trv': { state: 'heat', attributes: { hvac_action: 'idle' } },
|
||
'switch.trv_anti_scaling': { state: 'on' },
|
||
}) === '';
|
||
|
||
// выключена → чёрная
|
||
o.offIsDark = cls(trv, {
|
||
'climate.trv': { state: 'off', attributes: { hvac_action: 'off' } },
|
||
}) === '';
|
||
|
||
// Некоторые интеграции не отдают hvac_action: тогда их текущий режим из
|
||
// hvac_modes — единственный доступный сигнал фактической работы.
|
||
o.modeWithoutActionIsYellow = cls(trv, {
|
||
'climate.trv': { state: 'heat_cool', attributes: { hvac_modes: ['off', 'heat', 'cool', 'heat_cool'] } },
|
||
}) === 'on';
|
||
|
||
// Функциональная роль настоящей лампы желтит значок. Auxiliary light у
|
||
// soundbar/media-player, наоборот, отсечён системным role-resolver тестом.
|
||
const lamp = { id: 'l', primary: 'light.lamp', entities: ['sensor.lamp_power', 'light.lamp'], marker: null };
|
||
o.litLightWins = cls(lamp, {
|
||
'sensor.lamp_power': { state: '5' },
|
||
'light.lamp': { state: 'on' },
|
||
}) === 'on';
|
||
o.darkLampIdle = cls(lamp, {
|
||
'sensor.lamp_power': { state: '5' },
|
||
'light.lamp': { state: 'off' },
|
||
}) === '';
|
||
|
||
// «источник света»: умный выключатель с глупыми светильниками — жёлтый от
|
||
// того же условия, что и пятно glow
|
||
const wall = { id: 'w', primary: 'sensor.w', entities: ['sensor.w'],
|
||
marker: { is_light: true, controls: ['switch.fixtures'] } };
|
||
o.forcedSourceYellow = cls(wall, {
|
||
'sensor.w': { state: '1' }, 'switch.fixtures': { state: 'on' },
|
||
}) === 'on';
|
||
return o;
|
||
});
|
||
const working = page.locator('.dev.on').first();
|
||
const neutral = page.locator('.dev:not(.on):not(.open):not(.alarm):not(.unavail)').first();
|
||
out.hasWorkingAndNeutralMarkers = await working.count() > 0 && await neutral.count() > 0;
|
||
if (out.hasWorkingAndNeutralMarkers) {
|
||
const visual = (locator) => locator.evaluate((el) => {
|
||
const style = getComputedStyle(el);
|
||
return {
|
||
background: style.backgroundColor,
|
||
color: style.color,
|
||
shadow: style.boxShadow,
|
||
z: style.zIndex,
|
||
};
|
||
});
|
||
const restingWorking = await visual(working);
|
||
const restingNeutral = await visual(neutral);
|
||
out.workingUsesNeutralShadow = restingWorking.shadow === restingNeutral.shadow
|
||
&& !/255,\s*212,\s*92/.test(restingWorking.shadow);
|
||
await working.hover();
|
||
await page.waitForTimeout(180);
|
||
const hoverWorking = await visual(working);
|
||
await neutral.hover();
|
||
await page.waitForTimeout(180);
|
||
const hoverNeutral = await visual(neutral);
|
||
out.workingHoverMatchesNeutral = hoverWorking.background === hoverNeutral.background
|
||
&& hoverWorking.color === hoverNeutral.color
|
||
&& hoverWorking.shadow === hoverNeutral.shadow
|
||
&& hoverWorking.z === '5';
|
||
await page.mouse.move(0, 0);
|
||
await page.waitForTimeout(180);
|
||
const restoredWorking = await visual(working);
|
||
out.workingReturnsToYellow = restoredWorking.background === restingWorking.background
|
||
&& restoredWorking.color === restingWorking.color;
|
||
} else {
|
||
out.workingUsesNeutralShadow = false;
|
||
out.workingHoverMatchesNeutral = false;
|
||
out.workingReturnsToYellow = false;
|
||
}
|
||
await finish(browser, checkAll(out));
|