Files
houseplan-card/demo/smoke_value_format.mjs
T
MatyshandCursor 31cd4142ac feat(dev): furniture, hide layers, styling hooks, wall-thickness spec
Ship the unreleased 1.59 batch on dev: top-view furniture in the decor
layer, space toggles to hide decor/openings, stable card-mod data-*
hooks, HA entity value formatting, and the approved wall-thickness
spec (docs only — not implemented yet).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 11:41:00 +03:00

127 lines
7.8 KiB
JavaScript

// Значения форматирует HOME ASSISTANT (docs/STYLING-HOOKS.md §6, docs/LIVE-TEXT.md §2.1).
// Везде, где карточка печатает состояние ОДНОЙ сущности, она зовёт
// hass.formatEntityState (и hass.formatEntityAttributeValue для атрибута) —
// значит, работают display_precision, локальный разделитель дробной части и
// переводы состояний, а единица не дублируется.
// 1) бейдж display:'value' — «68,4 %» вместо сырых «68.42 %»;
// 2) живой текст декора — то же, плюс перевод 'on' → «Включено»;
// 3) своя единица заменяет единицу форматтера, а не приписывается к ней;
// 4) атрибут идёт через СВОЙ форматтер, а не через форматтер состояния;
// 5) старый HA без форматтера — прежнее поведение слово в слово;
// 6) плашки °/% остаются нашими: это производное значение, а не состояние.
// ПАДАЕТ на сборке до этих правок: бейдж и живой текст печатают сырое
// состояние, 'on' остаётся 'on', а своя единица приписывается к чужой.
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 sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const base = window.__mkHass();
// ---- сущности стенда: датчик с «лишними» знаками и выключатель ----
const EXTRA = {
'sensor.tank': { entity_id: 'sensor.tank', state: '68.42',
attributes: { friendly_name: 'Бак', unit_of_measurement: '%' } },
'switch.pump': { entity_id: 'switch.pump', state: 'on',
attributes: { friendly_name: 'Насос' } },
'climate.hall': { entity_id: 'climate.hall', state: 'heat',
attributes: { friendly_name: 'Котёл', current_temperature: 21.53, unit_of_measurement: '°C' } },
};
// Стаб ровно того, что делает HA: display_precision = 1, русская локаль
// (запятая), перевод состояний и единица ВНУТРИ результата.
const haFormat = (st) => {
if (st.state === 'on') return 'Включено';
if (st.state === 'off') return 'Выключено';
if (st.state === 'heat') return 'Нагрев';
if (st.state === 'locked') return 'Заперто';
const n = Number(st.state);
if (!Number.isFinite(n)) return st.state;
const txt = n.toFixed(1).replace('.', ',');
const u = st.attributes?.unit_of_measurement;
return u ? `${txt} ${u}` : txt;
};
const haAttr = (st, a) => String(st.attributes?.[a]).replace('.', ',');
const mk = (withFormatter) => {
const h = {
...base,
states: { ...base.states, ...EXTRA },
entities: { ...base.entities,
'sensor.tank': { entity_id: 'sensor.tank', platform: 'demo' },
'switch.pump': { entity_id: 'switch.pump', platform: 'demo' },
'climate.hall': { entity_id: 'climate.hall', platform: 'demo' } },
};
if (withFormatter) { h.formatEntityState = haFormat; h.formatEntityAttributeValue = haAttr; }
else { delete h.formatEntityState; delete h.formatEntityAttributeValue; }
return h;
};
const apply = async (withFormatter) => {
c.hass = mk(withFormatter);
c._regSignature = ''; c._cfgEpoch++; c._maybeRebuildDevices(); c.requestUpdate();
await c.updateComplete; await sleep(60); await c.updateComplete;
};
// ================= 1. бейдж «значение вместо значка» =================
c._serverCfg = { ...c._serverCfg, markers: [
{ id: 'm_tank', binding: 'entity:sensor.tank', space: c._space, display: 'value' },
] };
await apply(true);
const badge = () => sr().querySelector('[data-hp="device"][data-id="m_tank"] .valtext');
out.badgeRendered = !!badge();
out.badgeIsHaFormatted = badge()?.textContent.trim() === '68,4 %';
out.badgeUnitNotDoubled = !/%\s*%/.test(badge()?.textContent || '');
// старый HA без форматтера — ровно прежняя строка
await apply(false);
out.badgeFallsBackToRawState = badge()?.textContent.trim() === '68.42 %';
await apply(true);
// ================= 2. живой текст декора =================
c._curSpaceCfg.decor = [
{ id: 'lt_val', kind: 'text', x: 0.2, y: 0.92, text: 'Бак {}', color: '#123456', entity: 'sensor.tank' },
{ id: 'lt_unit', kind: 'text', x: 0.5, y: 0.92, text: '{}', color: '#123456', entity: 'sensor.tank', unit: 'проц.' },
{ id: 'lt_state', kind: 'text', x: 0.8, y: 0.92, text: 'Насос: {}', color: '#123456', entity: 'switch.pump' },
{ id: 'lt_attr', kind: 'text', x: 0.2, y: 0.96, text: '{}', color: '#123456', entity: 'climate.hall', attr: 'current_temperature' },
{ id: 'lt_attru', kind: 'text', x: 0.5, y: 0.96, text: '{}', color: '#123456', entity: 'climate.hall', attr: 'current_temperature', unit: '°C' },
];
c._cfgEpoch++; c.requestUpdate(); await c.updateComplete;
const txt = (id) => sr().querySelector(`text.dtext[data-id="${id}"]`)?.textContent.trim();
out.liveTextIsHaFormatted = txt('lt_val') === 'Бак 68,4 %';
out.liveTextUnitNotDoubled = txt('lt_val') === 'Бак 68,4 %' && !/%\s*%/.test(txt('lt_val'));
out.liveTextOwnUnitReplacesTheFormatters = txt('lt_unit') === '68,4 проц.';
out.liveTextStateIsTranslated = txt('lt_state') === 'Насос: Включено';
// атрибут идёт через СВОЙ форматтер: иначе тут было бы «Нагрев»
out.attrUsesTheAttributeFormatter = txt('lt_attr') === '21,53';
out.attrDoesNotInheritTheEntityUnit = txt('lt_attr') === '21,53';
out.attrTakesAnExplicitUnit = txt('lt_attru') === '21,53 °C';
// без форматтера — прежнее поведение слово в слово
await apply(false);
out.liveTextFallsBackToRawState = txt('lt_val') === 'Бак 68.42 %';
out.liveTextFallbackTranslatesNothing = txt('lt_state') === 'Насос: on';
out.attrFallsBackToTheRawAttribute = txt('lt_attr') === '21.53';
await apply(true);
// ================= 3. инфо-карточка =================
const dev = c._devices.find((d) => d.primary === 'lock.front_door');
c._infoCard = dev; c.requestUpdate(); await c.updateComplete;
const rowVals = [...sr().querySelectorAll('.entlist .entrow')]
.map((r) => r.querySelector('.entbtn, .ev')?.textContent.trim());
out.infoCardUsesTheFormatter = rowVals.includes('Заперто');
out.infoCardPrintsNoRawState = !rowVals.includes('locked');
c._infoCard = null; c.requestUpdate(); await c.updateComplete;
// ================= 4. плашки °/% остались нашими =================
// термометр в режиме «значение» по-прежнему показывает компактное «22.4°»:
// это ПРОИЗВОДНОЕ показание (среднее по датчикам зоны / атрибут климата),
// а не состояние одной сущности, и весь план читается как один прибор
c._serverCfg = { ...c._serverCfg, markers: [
{ id: 'd_temp', binding: 'device:d_temp', display: 'value' },
] };
await apply(true);
const tempBadge = [...sr().querySelectorAll('.dev.valonly .valtext')].map((e) => e.textContent.trim());
out.tempPlateKeepsItsCompactForm = tempBadge.includes('22.4°');
return out;
});
checkAll(res);
await finish(browser, res);