docs: specify selectable value face source

Issue: #378
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-29 22:17:11 +03:00
parent 6e1d93648a
commit a851c8f958
2 changed files with 338 additions and 1 deletions
+336
View File
@@ -0,0 +1,336 @@
# 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-флаг; публичный результат нормативен,
внутренняя форма нет.
+2 -1
View File
@@ -1,6 +1,6 @@
# Спецификации задач
Актуально на 2026-08-25.
Актуально на 2026-08-29.
GitHub Issues и GitHub Projects (v2) остаются единственным каноническим backlog проекта. Этот каталог содержит развёрнутые ТЗ: каждое ТЗ ссылается на issue, а issue — на соответствующий файл. Статус, приоритет и факт завершения меняются только в GitHub.
@@ -113,6 +113,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#75](https://github.com/Matysh/houseplan-card/issues/75) + [#76](https://github.com/Matysh/houseplan-card/issues/76) Единый поток размещения проёмов | [075-076-opening-placement-flow.md](075-076-opening-placement-flow.md) |
| [#84](https://github.com/Matysh/houseplan-card/issues/84) Источник «Всегда» без сущности HA + [#88](https://github.com/Matysh/houseplan-card/issues/88) ведущая сущность | [084-passive-forced-light-sources.md](084-passive-forced-light-sources.md) |
| [#90](https://github.com/Matysh/houseplan-card/issues/90) Управляемый бейдж со значением | [090-device-value-badge.md](090-device-value-badge.md) |
| [#378](https://github.com/Matysh/houseplan-card/issues/378) Выбираемый источник для режима «Значение + состояние» | [378-value-face-source.md](378-value-face-source.md) |
| [#94](https://github.com/Matysh/houseplan-card/issues/94) Универсальное действие «Переключить состояние» | [094-universal-state-toggle.md](094-universal-state-toggle.md) |
| [#101](https://github.com/Matysh/houseplan-card/issues/101) Плавный переход View ↔ редакторы | [101-view-editor-transition.md](101-view-editor-transition.md) |
| [#107](https://github.com/Matysh/houseplan-card/issues/107) Переключение виртуального источника света «Всегда» | [107-virtual-light-toggle.md](107-virtual-light-toggle.md) |