Files
houseplan-card/demo/smoke_value_format.mjs
T
Claudeandclaude[bot] 5a258f3128 docs(hygiene): свести дубли документов подсистем и снять устаревшее (#679)
Волна 2 эпика #674 — у каждого правила один дом, остальные места ссылаются.

DECOR-EDITOR.md ← BACKDROP.md + LIVE-TEXT.md: один документ с нумерованными
разделами (§3 подложка, §5 текст с живыми значениями), на которые теперь
указывают комментарии кода вместо несуществовавших «BACKDROP §2/§3»;
исправлено утверждение, что space-card не рисует декор (он рисует подложку
и картинки декора, но не фигуры, мебель и текст). LIGHT.md ← матрица
настроек света (перевод, тест назван явно: test/devices.test.mjs «issues
84/88»). DEVICE-PRESENTATION.md ← правила «что показывает маркер» из
FILTERING.md (порядок cover → light sources → device role, шторы,
медиаплееры); «в одном pull request» → «в одном коммите». CANVAS.md: §9.5
«Оптимизировать планы» → CONFIG-COMPATIBILITY.md, overlay и планарные грани
Walls → WALL-THICKNESS.md §10–11, таблицы «было/стало» сняты. TESTING-DEMO.md
→ demo/stand/README.md: карта демо-дома и «чего на стенде нет», ручной
чек-лист снят (ручной фазы в процессе нет). ISOMETRIC.md — только текущее;
история Stage 2/4 — docs/adr/570-isometric-stage4-visual-handoff.md.
SUN.md: удалённый контракт фона снят, правило бумаги — в текущем разделе.
UX-MODES.md: декор над заливками, а не «под комнатами»; «hidden isometric»;
follow-up из #3 — все выпущены. Шапки VACUUM, WARM-REMOUNT («Выровнять всё
по сетке» → «Оптимизировать планы»), WALL-THICKNESS, STYLING-HOOKS,
CONFIG-COMPATIBILITY (#33), PDF-EXPORT — без устаревших статусов и планов.
README EN/RU: абзац про пересъёмку скриншотов → CONTRIBUTING.md, RADAR и
PDF-EXPORT в списке документации, RU догнал EN (2.5D, повторное
использование загруженного изображения, STAIRS). Один список канонических
документов подсистем в AGENTS.md и промпте ревьюера (_process.yml).
WALL-THICKNESS.md ссылается на ADR 282.

Правки src/** и validation.py — только пути документов в комментариях.

Issue: #679
User-Visible: no
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 18:07:37 +00:00

127 lines
7.8 KiB
JavaScript

// Значения форматирует HOME ASSISTANT (docs/STYLING-HOOKS.md §6, docs/DECOR-EDITOR.md §5.2).
// Везде, где карточка печатает состояние ОДНОЙ сущности, она зовёт
// 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);