21 KiB
Issue #318 — активный контроллер без собственных сущностей следует controls
- Дата: 2026-08-28
- Тип: bug · приоритет P2
- Оценка: пользовательская ценность 6/10 · сложность 3/10 · риск 4/10
- Issue: #318
- Связанные контракты: #251, #267, #274
- Ветка:
issue/318-switch-render-state
Канонические документы: docs/SCOPE.md, docs/ARCHITECTURE.md,
docs/DEVICE-PRESENTATION.md, docs/CONFIG-COMPATIBILITY.md,
docs/TOUCH-SUPPORT.md, docs/USER-GUIDE.md,
docs/USER-GUIDE.ru.md, docs/TESTING.md.
1. Сценарий и персона
Житель дома смотрит в View или kiosk на физический настенный выключатель,
который управляет лампой либо группой через сохранённые controls. Home
Assistant содержит активную запись устройства, но интеграция не создаёт для
него ни одной собственной сущности. При переключении управляемого света
человек ожидает увидеть обычный нейтральный выключатель для off и жёлтый —
для on, а не постоянный признак недоступного устройства.
Поверхности: основной план в View/kiosk, hosted Static и живой preview редактора
устройств. Это J1 «что происходит сейчас» и J3 «быстрое очевидное действие» из
docs/SCOPE.md.
2. Что человек увидит до и после
До: выключатель постоянно выглядит недоступным независимо от того, включается или выключается управляемый свет.
После: при включённой цели выключатель становится жёлтым, при выключенной или недоступной цели — обычным нейтральным; приглушение сохраняется только там, где Home Assistant действительно предоставляет собственные сущности контроллера и все они не имеют живого состояния.
3. Проблема и подтверждённый диагноз
Полевой сценарий воспроизведён на dev.houseplan.tech. Сохранённый marker
Wall Switch Kitchen имеет активный device: binding, ноль собственных строк
entity registry и controls: [light.wall_lights].
resolvePresentationSources() корректно берёт working/neutral у
управляемой цели. Затем resolveDevicePresentation() классифицирует marker как
controller face и вызывает controllerAvailability(). Функция ищет хотя бы
одно живое состояние в DevItem.entities; пустой список безусловно даёт
unavailable. Presentation policy заменяет только availability агрегата,
поэтому target status обновляется внутри проекции, но класс unavail имеет
визуальный приоритет и скрывает и жёлтую, и нейтральную подложку.
Обновление HA-состояний и repaint исправны: на приложенных кадрах одновременно меняются лампы и комнатный счётчик. #274 также уже гарантирует одинаковый roster и одну semantic generation для плана и preview. Дефект находится не в snapshot continuity, а в неразличении двух случаев: «у контроллера нет канала телеметрии» и «его существующие каналы сообщают unknown/unavailable».
4. Зафиксированное решение владельца
- Активный физический
device:binding с пустым собственным roster считается доступным: отсутствие телеметрии не является доказательством offline. - Его
working/neutralследует существующему resolved graphcontrols: доступная цельonдаёт жёлтую подложку, все доступные целиoff— нейтральную. - Если roster непуст, но ни одна собственная сущность не имеет живого
состояния, контроллер остаётся
unavailableпо контракту #251. - Явные
ha_disabled,orphaned, virtual controller, partial controls, безопасный no-op и приоритет alarm не меняются. - Plan, preview и hosted Static используют один результат общего presentation resolver.
Продуктовых вопросов не осталось.
5. Скоуп
Входит
- различение пустого и непустого собственного roster для активного физического device-binding;
- доступная проекция пустого roster с target-derived
working/neutral; - сохранение
unavailableдля непустого roster без живых состояний; - совпадение View, kiosk, hosted Static и device preview;
- актуализация строки S05 таблицы решений #267 и RU/EN документации;
- unit, production-bundle smoke и mutation evidence;
- оба changelog.
Не входит
- считать запись device registry доказательством online, когда у устройства есть собственные сущности;
- менять entity-bound markers: активная
entity:привязка содержит точную сущность и не образует пустой roster; - новый badge, warning glyph, pulse, цвет или текст причины;
- изменение resolver управляемых целей, Toggle, confirmation или toast;
- изменение Glow, room fill, light statistics или service payload;
- исправление/миграция данных dev-стенда;
- persisted config, backend, schema/model version или registry API;
- расширение поведения на
unverified,ha_disabledлибоorphanedbinding.
6. Контракт поведения
6.1. Термины
own roster — DevItem.entities, то есть активные собственные сущности exact
binding после действующих registry-фильтров. roster empty означает длину 0, а
не список из сущностей без live state.
active physical device binding выполняет одновременно:
bindingKind === 'device'либоmarker.bindingначинается сdevice:;bindingStatus.kind === 'active';- marker не virtual.
Отсутствующий bindingStatus, unverified, ha_disabled и orphaned не дают
права применять fallback. Техническая реализация вправе выразить этот
предикат иначе, если наблюдаемый контракт остаётся тем же.
6.2. Матрица availability/status
При включённых live states и сохранённых внешних controls:
| Binding и собственный roster | Управляемые цели | Итоговая проекция |
|---|---|---|
| active device, roster пуст | хотя бы одна доступная on |
available + working; жёлтая |
| active device, roster пуст | все доступные off |
available + neutral |
| active device, roster пуст | все unavailable/missing/отфильтрованы tombstone | available + neutral |
| active device, roster непуст, хотя бы одна own state живая | target on |
available + working |
| active device, roster непуст, хотя бы одна own state живая | targets off/unavailable |
available + neutral |
| active device, roster непуст, все own missing/unknown/unavailable | target on |
unavailable; faded имеет приоритет над working |
| active device, roster непуст, все own missing/unknown/unavailable | targets off/unavailable | unavailable + neutral |
| virtual controller, roster пуст | target on/off/unavailable | действующий контракт virtual controller без изменений |
| ha-disabled/orphaned device, roster пуст | любое | действующий lifecycle-контракт без fallback active-device |
Живым остаётся состояние, отличное после trim/lowercase от пустой строки,
unknown и unavailable. Числа, on, off, battery, LQI и update считаются
живыми, как в #251.
Если target aggregate недоступен, controller override меняет availability на
available, но не создаёт ложный working: итог нейтрален. Если цель доступна
и включена, target-derived working сохраняется.
6.3. Приоритеты
- critical alarm собственной сущности остаётся выше availability/status;
live_states: falseиstatic_iconсохраняют нейтральную статичную подачу;- HA-disabled/user-hidden/orphaned lifecycle не оживает от пустого roster;
controlsопределяют status, но не становятся собственными сущностями;- target tombstone сохраняет controller role по #274 и даёт нейтральное лицо;
- partial target group и исполнение действия не меняются;
- LQI/value badge/температура не создаются из пустого roster.
6.4. A11y, hover и действия
Доступный marker снова получает обычный hover/focus paint и a11y-state
working либо neutral вместо unavailable. Hit area, click/tap target,
confirmation и service calls не меняются. Touch не получает нового жеста;
View/kiosk остаются полностью поддержанными.
7. UX
Новых настроек и элементов нет. Изменение происходит автоматически после обновления frontend: существующий marker перестаёт быть полупрозрачным и использует уже знакомые нейтральную/жёлтую подложки.
Preview редактора обязан показывать тот же результат для несохранённого draft, построенного из того же active binding и полного sibling roster. Hosted Static показывает ту же live проекцию, оставаясь неинтерактивным согласно своей поверхности. Светлая/тёмная тема меняет только существующие theme tokens.
8. Модель данных, compatibility и migration
Persisted marker, controls, binding, config/model version, backend storage и
HA registry не меняются. Миграции нет. Старый frontend продолжит показывать
пустой controller roster приглушённым; новый применит правило при следующем
render/state tick без записи конфигурации.
Fallback основан на уже вычисленном bindingStatus и не должен добавлять
registry fetch, polling либо серверное device-health состояние.
9. i18n и документация
Новых пользовательских строк нет, поэтому JSON-каталоги en/ru/de не меняются. Обновляются:
docs/DEVICE-PRESENTATION.md: S05 разделяется на empty-active и non-empty-without-live варианты;docs/ARCHITECTURE.md: уточняется семантика controller availability;docs/USER-GUIDE.mdиdocs/USER-GUIDE.ru.md: отсутствие собственных сущностей у активного устройства не трактуется как offline;docs/TESTING.md: добавляется точная матрица регрессии;docs/CHANGELOG.mdиdocs/CHANGELOG.ru.md: короткий user-visible bullet со ссылкой на #318.
10. Критерии приёмки
| AC | Требование | Доказательство |
|---|---|---|
| AC1 | Exact fixture device:active, own roster [], controls=[target] даёт available+working для target on и available+neutral для off |
test/device-presentation.test.mjs |
| AC2 | Та же fixture при target unavailable/missing и при target-marker tombstone остаётся available+neutral, без ложного working/pulse | unit matrix + production-bundle smoke |
| AC3 | Непустой roster, в котором все own states missing/unknown/unavailable, остаётся faded даже при target on; живые battery/LQI/update сохраняют #251 | существующие и новые negative units |
| AC4 | ha_disabled, orphaned, virtual controller, no-controls marker, static icon, live_states off и alarm сохраняют канонические строки таблицы решений |
policy/presentation regression suite |
| AC5 | Plan и device preview для active empty-roster controller совпадают по sourceKind, visual, classes и a11y state; target on→off обновляет обе поверхности |
расширенный demo/smoke_wireless_controller_parity.mjs |
| AC6 | Hosted Static использует ту же presentation policy; light/dark и desktop/touch не вводят новый layout/gesture | shared resolver unit + smoke/golden regression |
| AC7 | Config round-trip, controls и service-call payload не меняются; backend/model/schema untouched | diff audit + existing config/toggle tests |
| AC8 | Мутант, возвращающий empty active roster к unavailable, ловится целевым тестом; существующие мутанты #251/#274 продолжают ловиться |
scripts/mutation-gate.mjs direct run |
| AC9 | Fast gates, выбранные targeted smokes, docs check, bundle sync/budget зелёные | точные команды в handoff issue |
11. План реализации и автотестов
- В
controllerAvailability()либо чистом соседнем policy helper различить active physical device с roster[]и non-empty roster без live states. - Не менять
resolvePresentationSources()и target graph: существующий aggregate уже правильно вычисляетworking/neutral. - Добавить unit-матрицу AC1–AC4 в
test/device-presentation.test.mjsи при необходимости чистый policy test для active-binding gate. - Расширить
demo/smoke_wireless_controller_parity.mjsвторым exact fixture без собственных registry rows; проверить plan/preview и on/off/unavailable. - Добавить узкий mutation guard empty-roster fallback; повторно прогнать
controller-availability-follows-targetи мутанты #274. - Обновить канонические документы и оба changelog.
- Перед
S7-code-reviewвыполнить минимум:
npm run typecheck
npm test
npm run build
npm run bundle:sync
npm run bundle:budget
node scripts/check-docs.mjs
node scripts/smoke-select.mjs --base origin/dev --head HEAD
node demo/smoke_wireless_controller_parity.mjs
node scripts/mutation-gate.mjs --id=<new-empty-roster-mutant>
node scripts/mutation-gate.mjs --id=controller-availability-follows-target
node scripts/mutation-gate.mjs --id=wireless-controller-loses-filtered-target-role
node scripts/mutation-gate.mjs --id=wireless-controller-preview-drops-sibling-markers
Если smoke-select выберет дополнительные smokes, они также обязательны.
Golden baseline не принимается в реализации; полный golden:verify остаётся
предрелизным gate.
12. Производительность, безопасность и privacy
Проверка ограничена одним уже построенным marker: O(1) для пустого списка и
существующий O(e) для non-empty roster. Нельзя повторно строить light graph,
запрашивать registry/backend или вводить новый cache.
Действие и service payload не меняются; недоступные цели по-прежнему fail-closed. Новых сетевых запросов, разрешений, логируемых identifiers и персональных данных нет. Решение не должно выводить entity/device IDs в UI.
13. Риски и меры
| Риск | Мера |
|---|---|
| Пустой roster ошибочно оживит disabled/orphaned marker | fallback требует точного bindingStatus.kind === active; lifecycle negative tests |
| Общий fallback ослабит #251 для event-only/unknown устройств | различать length === 0 и non-empty; exact negative unit/mutant |
| Target unavailable станет ложным working | статус не синтезировать; сохранять target aggregate и проверить AC2 |
| Plan и preview снова разойдутся | production-bundle smoke сравнивает полный ResolvedDevicePresentation |
| Entity marker случайно получит fallback | ограничить active physical device: binding и проверить negative fixture |
| Изменится Toggle/Glow | не менять соответствующие resolver; existing suites + diff audit |
14. Откат
Откат — вернуть прежнюю ветку empty roster → unavailable и сопровождающие
документы/tests. Данных для обратной миграции нет: конфигурация не меняется.
Если до беты выяснится, что active device registry недостаточно как fallback,
issue возвращается в S3-spec; нельзя маскировать риск новым persisted flag без
отдельного продуктового решения.
15. Release-артефакты
- user-visible: yes;
- один короткий пункт со ссылкой на #318 в RU и EN changelog;
- release body упоминает исправление только если оно входит в публикуемую beta/stable range; мелкие внутренние проверки отдельно не перечисляются;
- issue закрывает релиз-менеджер после зелёной опубликованной беты;
- новые screenshot/golden baseline не требуются, если semantic smoke и существующая visual matrix зелёные без ожидаемой baseline-дельты.
16. Принятые технические предположения — менять свободно
- Наиболее узкое место изменения —
controllerAvailability(hass, d); перенос предиката в чистую presentation policy допустим, если источник истины один. bindingStatus— достаточное техническое доказательство active binding; сам HA device registry не объявляется runtime health API.- Existing
combineVisualSamples()и controller-face override уже дают правильный status; отдельный новый enum availability не нужен. - Existing #274 smoke дешевле и надёжнее расширить вторым сценарием, чем создавать ещё один browser process.
- Decision trace может получить отдельный внутренний ID для empty-roster fallback; это developer-facing изменение без persisted schema и i18n.