Files
houseplan-card/docs/specs/378-value-face-source.md
2026-08-29 22:17:11 +03:00

337 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue #378 — выбираемый источник для режима «Значение + состояние»
- **Issue:** https://github.com/Matysh/houseplan-card/issues/378
- **Статус документа:** первая редакция, готова к ревью
- **Приоритет / тип:** P2 · feature / polish
- **Область:** frontend presentation + editor, marker config, backend validation,
import/export, документация и QA
- **Связи:** #90 (канонические источники value badge), #321 (пользовательский
сценарий position only)
- **Ревизия:** 1 (2026-08-29)
## Сценарий
Администратор дома на desktop открывает диалог уже размещённой маркизы, выбирает
режим «Значение + состояние», а затем источник «Положение · Маркиза». Preview
сразу показывает `42 %`. После сохранения то же значение видят на полном плане и
в static space card владелец, член семьи и гость; нажатие по маркеру по-прежнему
управляет маркизой по существующей настройке действия.
## Что человек увидит до и после
**До:** режим показывает слово `Open`, потому что умеет брать только state
автоматически выбранной сущности. **После:** пользователь одним селектором
выбирает полезный показатель и видит `42 %` вместо иконки, без дополнительного
слова и без template sensor.
## Проблема
`resolveValue()` в `src/device-presentation.ts` выбирает одну visual entity и
передаёт её в `validStateValue()`. Атрибут выбрать нельзя. В то же время
`src/device-value-badge.ts` уже содержит:
- канонический набор `ValueBadgeSource`;
- построение кандидатов текущего устройства;
- allowlist поддерживаемых атрибутов;
- преобразования brightness/volume/percentage/temperature;
- unavailable-контракт и стабильные ключи источников.
Создание второй таблицы или второго formatter приведёт к расхождению двух
селекторов. Простого добавления поля в диалог тоже недостаточно: конфиг проходит
backend delta-validation, ссылки `marker:*` переписываются при rebind и
переносятся между пространствами через import/export.
## Скоуп
- Отдельный селектор источника лица значения в диалоге устройства при
`display: value`.
- Явный источник state, поддерживаемого attribute либо derived value из ровно
того же набора и в том же порядке, что value badge.
- Общий resolver и formatter для value badge и внутреннего лица значения.
- Live preview и одинаковый результат в основном плане и static space card.
- Опциональное поле marker config, lossless delta-validation, rebind и полный
import/export reference seam.
- Обновление документации режима и decision table.
## Не-скоуп
- Новые атрибуты, единицы, формулы, шаблоны либо произвольный entity picker.
- Изменение внешнего value badge или положения его капсулы.
- Новый display mode.
- Изменение визуального состояния подложки, activity, pulse, alarm, Glow,
room metrics или LQI palette.
- Изменение tap/click target и действий `info`, `toggle`, `run`, `cover`.
- Выбор нескольких значений либо prefix/suffix пользователя.
## Контракт поведения
### 1. Выбор источника
1. При `display: value` сразу под подсказкой режима отображается поле
«Источник значения».
2. Первая строка — «Автоматически (как раньше)». Она соответствует отсутствию
явного источника. Далее идут кандидаты `valueBadgeCandidates()` в том же
порядке, с теми же названиями, техническими подписями и текущими значениями,
что в selector value badge.
3. Оба потребителя используют общий `ValueSource`/`ValueSourceCandidate`, общий
source-key codec и общий formatter/resolver. Совпадение списков доказывается
тестом на одну и ту же ссылку/проекцию данных, а не сравнением двух копий
allowlist.
4. Отсутствие кандидатов не запрещает legacy auto: строка «Автоматически»
остаётся доступной. Для virtual marker selector disabled с существующим
объяснением `value_virtual`; сохранённый явный источник не удаляется.
5. Переключение на другой display mode скрывает поле, но сохраняет выбор.
Возврат в `value` восстанавливает его.
6. При явной смене HA binding в диалоге старый `value_source` сбрасывается в
auto: перенос источника прежнего устройства был бы ложной настройкой.
### 2. Разрешение и форматирование
1. Если `marker.value_source` отсутствует или `null`, вызывается существующий
auto-path `resolveValue()` без изменения порядка климатической legacy-
эвристики, power-gate и проверки неоднозначности.
2. Если источник задан явно, auto-path не выбирает и не подставляет другую
сущность. State, attribute, `derived_lqi` и `derived_marker_state`
разрешаются тем же кодом, что value badge.
3. `cover.current_position = 42` отображается `42 %`; brightness, volume,
humidity, battery и temperature получают ровно те же преобразования и
единицы, что внешний badge. `valueText` — компактный текст,
`valueFullText` — доступное полное значение/tooltip.
4. Временно отсутствующий, unknown/unavailable или переставший быть скалярным
**явный** источник сохраняется и отображает `—` внутри лица значения. Он не
заменяется иконкой и не переключается на auto. Presentation сохраняет
диагностический код `value_no_state` или `value_non_scalar`, но explicit-
ветка не трактует этот код как разрешение сменить лицо.
5. Для auto-path прежняя семантика остаётся: `value_no_state`,
`value_ambiguous_sources` и `value_non_scalar` откатывают лицо к иконке.
Явный источник по определению снимает `value_ambiguous_sources`.
6. `value_virtual` имеет приоритет над сохранённым источником: virtual marker
остаётся с иконкой, как сейчас.
7. Ключ выбранного источника входит в `sourceSignature` даже в unavailable-
состоянии, чтобы смена выбора/восстановление данных инвалидировала snapshot.
8. Если внешний badge показывает тот же source key, существующая подсказка о
дублировании остаётся и сравнивает уже явный источник лица.
### 3. Preview и действия
- Preview строит draft marker с текущим `display` и `value_source` и вызывает
тот же `resolveDevicePresentation()`, что оба plan renderer; отдельной mock-
строки `42 %` нет.
- Смена selector обновляет preview без Save.
- Сохранение не требуется для preview, но Cancel не пишет draft в config.
- Выбор источника не меняет `primary`, visual sources, `tap_action`,
`tap_target`, `toggle_entity`, controls или more-info entity.
- Pointer и touch hit area не меняются; на touch результат и действие идентичны
desktop. View/киоск считаются release-blocking поверхностями.
## UX
Новый selector расположен рядом с причиной выбора режима, до блока внешнего
value badge. В нём переиспользуются строки кандидатов #90. Новые строки нужны
только для собственной подписи/help и auto-опции:
- «Источник значения»;
- «Выберите, что заменит иконку. Действие по нажатию не изменится»;
- «Автоматически (как раньше)»;
- missing-hint, который говорит, что лицо покажет `—`, пока источник не
восстановится или пользователь не выберет другой.
Сохранённый источник, которого нет среди текущих кандидатов, добавляется одной
выбранной disabled-looking, но сохраняемой строкой «Источник недоступен» по
паттерну value badge. Пользователь может оставить её, выбрать другой источник
или auto. Само открытие и сохранение диалога без изменения selector не
материализует рекомендацию и не удаляет неизвестное значение.
## Модель данных и миграция
### Frontend
В `Marker` добавляется:
```ts
value_source?: ValueBadgeSource | null;
```
Имя техническое и не обещает связь с tap target. Тип источника извлекается из
badge-модуля в нейтральный общий контракт либо реэкспортируется без дублирования.
- absence/`null` = legacy auto;
- object = explicit source;
- выбор auto удаляет поле при записи нового frontend-конфига;
- неизвестный untouched literal losslessly round-trips согласно
`docs/CONFIG-COMPATIBILITY.md`; после явной правки поля записывается только
канонический source либо отсутствие.
Миграции и массовой материализации нет: существующие marker остаются без поля и
выглядят как до #378.
### Backend validation
`MARKER_SCHEMA` принимает `value_source` как lossless object/`null`, а
семантическая delta-validation применяет к изменённому значению тот же набор
kind/полей/entity-id/attribute, что `value_badge.source`. Общая функция
валидации источника должна исключить второй backend allowlist.
Для `derived_marker_state` ref обязан вести на существующий не-removed marker с
`is_light: true`, как у badge. Полный импорт валидирует все новые данные;
обычная запись сохраняет untouched future literal.
### Reference seam и import/export
- `rewriteMarkerControlReferences()` переписывает `value_source.ref` вместе с
controls и `value_badge.source.ref` при смене marker id.
- Full export/import сохраняет поле штатно.
- Space export оставляет derived ref только если target входит в переносимый
набор; иначе удаляет `value_source` (возврат в auto) и увеличивает счётчик
dropped marker links.
- Space import remap-ит живой target на новый marker id. При skip/virtualize или
отсутствующем target поле удаляется, сырой `marker:old-id` не протекает.
- Виртуализация дубликата удаляет HA-dependent `value_source`, как
`value_badge`, `controls` и tap fields.
## i18n
Добавить с паритетом EN/RU/DE/FR:
- `marker.value_source`
- `marker.value_source.help` и `.aria`
- `marker.value_source_auto`
- `marker.value_source_missing_hint`
Имена state/attribute/derived-кандидатов и `marker.value_badge_missing`
переиспользуются. Тексты `marker.display_hint_value` во всех четырёх языках
обновляются: значение может быть выбранным, а не только «однозначным» auto.
## Затронутые файлы и модули
- `src/types.ts`, `src/device-value-badge.ts`, `src/device-presentation.ts`,
`src/device-presentation-policy.ts` (только explicit-dash gate),
`src/devices.ts`.
- `src/houseplan-card.ts`, `src/houseplan-editor-runtime.ts`.
- `src/i18n/{en,ru,de,fr}.json`.
- `custom_components/houseplan/validation.py`,
`custom_components/houseplan/import_export.py`, websocket validation call
sites при переименовании validator.
- Frontend/backend unit tests и целевой browser smoke.
- `docs/DEVICE-PRESENTATION.md`, `docs/ARCHITECTURE.md`,
`docs/CONFIG-COMPATIBILITY.md`, `docs/USER-GUIDE.md`,
`docs/USER-GUIDE.ru.md`, оба changelog.
## Критерии приёмки
- **AC1 — список и сохранение (unit + smoke).** В `display: value` selector
содержит auto и ровно те же кандидаты/порядок, что value badge; выбор
`cover.current_position` сохраняет `value_source`, повторное открытие
восстанавливает выбор, выбор auto удаляет поле.
- **AC2 — результат на всех renderer (unit + smoke + golden).** При position 42
preview, полный план и static space card показывают `42 %` вместо иконки;
desktop/touch tap вызывает тот же action/target, что до выбора.
- **AC3 — паритет formatter (unit).** Все поддерживаемые attribute kinds,
entity state, LQI и marker state дают одинаковые `text/fullText/availability`
для лица и value badge.
- **AC4 — legacy compatibility (unit + backend).** Marker без поля и с `null`
проходит прежние F09–F12 сценарии: auto state, ambiguity/no-state/non-scalar
fallback и virtual fallback не меняются; простое Save не материализует поле.
- **AC5 — fail explicit (unit + smoke).** Сохранённый явный источник, временно
исчезнувший либо unavailable, остаётся выбранным, на лице показывает `—`,
даёт корректный диагностический reason и не подменяется иконкой/другим state;
возврат source восстанавливает значение без повторного выбора.
- **AC6 — независимость (unit).** Выбор не меняет внешний badge, visual/activity
state, room aggregates, Glow, LQI palette, primary и tap resolver; duplicate-
hint срабатывает при совпадении source keys.
- **AC7 — конфиг и ссылки (backend + unit).** Delta-validation режет каждый
некорректный новый kind/attribute/entity/ref, сохраняет untouched future
literal, rebind переписывает derived ref, full transfer сохраняет, space
transfer remap-ит либо явно удаляет dangling ref с отчётом.
- **AC8 — preview/Cancel/binding (smoke).** Selector меняет draft preview сразу;
Cancel ничего не пишет; смена binding сбрасывает старый source в auto.
- **AC9 — локализация и документация (unit + docs gate).** Четыре словаря имеют
паритет, обновлены guide/presentation/architecture/compatibility, docs
screenshots соответствуют точному SHA.
- **AC10 — гейты и бюджет (commands).** `npx tsc --noEmit`, `npm test`,
`npm run build`, backend pytest, `no-new-any`, целевой smoke,
`golden:verify` и `check-docs` зелёные; bundle budget без существенной дельты.
## План автотестов
- Расширить unit `device-value-badge` общим resolver contract и таблицей всех
source kinds/formatter branches.
- Расширить `device-presentation`/decision fixtures: explicit position,
unavailable explicit dash, restored source, legacy F09–F12 без изменений,
source signature и duplicate badge.
- Unit editor save/draft: untouched, explicit, auto, missing, binding change,
Cancel.
- Unit `devices`: rebind `derived_marker_state` для обоих потребителей.
- Backend pytest: schema/delta-validation, future literal, renamed marker,
full/space import-export keep/remap/drop/virtualize.
- Новый либо расширенный целевой Playwright smoke диалога: preview + save +
reopen + unavailable/recovery + main/static render + desktop/touch action.
- Golden-сценарий с cover 42 % и выбранным source принимается только через
штатный reviewed Linux artifact; docs screenshots — только workflow
`Docs screenshots` на точном SHA.
Мутанты: удалить explicit branch → AC2 красный; заменить source formatter на
raw attribute → AC3; fallback explicit в auto/icon → AC5; не переписать ref →
AC7; материализовать auto при открытии → AC4/AC8.
## Производительность и безопасность
- Сеть, сервисы HA и частота обновлений не меняются.
- Candidate discovery уже кэшируется; один выбранный source разрешается O(1),
кроме существующего derived marker-state graph. Новый обход всех HA entities
запрещён.
- В snapshot/signature добавляется один короткий source key; заметной дельты
bundle/render budget не ожидается и она проверяется штатным budget gate.
- Backend принимает только те же ограниченные source shapes и attributes, что
badge; произвольный template/code/URL не исполняется. Security artifact не
требуется, кроме unit отрицательных входов.
## Touch
Сам selector — desktop-поверхность редактора. Результат живёт в View и киоске:
размер/hit target маркера и tap semantics не меняются. AC2 и AC6 обязаны
проверить touch pointer path; расхождение desktop/touch блокирует выпуск.
## Риски
- Две настройки с похожим названием: смягчается размещением source сразу под
display и сохранением отдельного заголовка «Бейдж со значением» ниже.
- Расхождение formatter/candidates: запрещено контрактом общей функции и AC3.
- Dangling `marker:*` после rebind/import: закрывается полным reference seam AC7.
- Изменение legacy auto при рефакторинге: закрывается неизменными decision
fixtures и AC4.
- `—` может быть принят за значение: missing warning и preview reason объясняют
восстановление; молчаливый fallback опаснее.
## Откат
`git revert` реализационного коммита. Старый frontend/backend losslessly
пропустит дополнительный marker key благодаря `ALLOW_EXTRA`, но проигнорирует
его и покажет legacy auto; данные не теряются. Повторная установка новой версии
восстановит выбор. Массовая очистка конфига не нужна.
## Release-артефакты
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`: user-visible пункт со ссылкой
#378 в том же коммите, что поведение.
- `docs/USER-GUIDE.md` и `.ru.md`: как выбрать position/percentage/temperature
и что tap не меняется.
- `docs/DEVICE-PRESENTATION.md`: explicit/auto/dash строки decision table.
- `docs/ARCHITECTURE.md` и `docs/CONFIG-COMPATIBILITY.md`: общий resolver,
optional key, delta-validation и reference seam.
- Golden cover 42 % + reviewed artifact; docs screenshots с точного SHA.
- Отдельного performance/security отчёта нет: результаты budget и негативных
validation tests входят в handoff.
## Принято предположительно, поменять свободно
Это технические решения, а не новые продуктовые требования:
- имя persisted-поля `marker.value_source`;
- извлечь neutral source module или реэкспортировать тип/функции из текущего
`device-value-badge.ts` — выбирается вариант с меньшим циклом зависимостей;
- расширить существующий smoke или создать отдельный;
- хранить diagnostic fallback рядом с explicit `—` отдельным полем либо снять
запрет fallback в policy через explicit-флаг; публичный результат нормативен,
внутренняя форма нет.