25 KiB
Issue #104 — HA-привязка проёма после удаления маркера
- Issue: https://github.com/Matysh/houseplan-card/issues/104
- Статус: первая редакция ТЗ; требуется независимое ревью
- Тип / приоритет: bug / P2
- Оценка: пользовательская ценность 8/10; сложность и риск 5/10
- Область: настройки проёма в Plan, состояние проёма и замка в View/киоске, политика доступности HA-привязок, документация и тесты
- Модель данных: без изменений и миграции
- Связано: #98,
docs/FILTERING.md,docs/USER-GUIDE.ru.md,docs/ARCHITECTURE.md,docs/CONFIG-COMPATIBILITY.md
1. Продуктовый контекст
Персона: пользователь Home Assistant, который убирает с плана лишний самостоятельный маркер датчика или замка, но продолжает использовать ту же сущность как часть двери, окна или ворот.
Поверхности и момент: Plan → свойства проёма при выборе датчика/замка; затем View или киоск, когда House Plan показывает фактическое состояние проёма и пользователь открывает карточку его замка.
До → после, без технических терминов: сейчас после удаления отдельного значка датчик исчезает из настроек, а дверь перестаёт показывать состояние; после исправления датчик остаётся частью двери, хотя отдельного значка на плане по-прежнему нет.
Это поддерживает основные jobs продукта:
- J2: с одного взгляда понимать, что открыто и что не заперто;
- J6: сохранять правдивый план при изменении состава устройств и маркеров;
- J3: выполнять действие с замком только через уже существующий явный и защищённый control карточки проёма.
2. Проблема и подтверждённая причина
Удаление маркера сохраняет минимальный marker.removed = true tombstone для
entity:* либо device:*. Это правильно запрещает повторное автоматическое
появление маркера и исключает его из агрегатов и живых источников плана.
Однако тот же фильтр сейчас применяется к отдельным полям проёма:
_contactCandidates()и_lockCandidates()используют_planEntityAvailable();_openingAmt(),_renderOpenings()и_renderOpeningLocks()используют_renderEntityAvailable();- карточка проёма и
_lockAction()снова используют_planEntityAvailable().
Обе проверки вызывают isRemovedPlanEntity(). Поэтому tombstone, который
описывает отсутствие самостоятельного маркера, ошибочно выключает явно
сохранённые opening.contact и opening.lock.
При этом immutable render snapshot уже захватывает contact и lock каждого
проёма. Дефект находится в политике доступности, а не в формате конфигурации или
доставке HA state в кадр.
3. Скоуп
В задачу входят:
- отдельная политика доступности явной HA-ссылки проёма;
- списки контактных датчиков и замков в свойствах проёма;
- анимация/тон двери, окна или ворот по контактному датчику;
- бейдж замка, карточка проёма и единственная разрешённая кнопка lock/unlock;
- случаи entity-tombstone и device-tombstone;
- сохранение текущей семантики HA-disabled, orphaned, limited registry и
unavailablestate; - unit-тесты, целевой browser smoke и актуализация пользовательского контракта.
4. Не входит в задачу
- восстановление, повторное создание или перемещение удалённого маркера;
- изменение
removedtombstone, списка Добавить или повторного добавления; - ослабление фильтрации LQI, температуры, влажности, света, Glow, live text, controls, vacuum и других plan-level contributions;
- новый picker, поиск, группировка, предупреждения или подписи в диалоге;
- новые состояния, цвета, иконки, анимации или геометрия проёмов;
- изменение правил lock/unlock, confirmation или разрешение действия по тапу на маркер/сам проём;
- изменение
houseplan-space-card: статическая карточка сейчас проёмы не рисует; - backend, schema version, import/export и миграция сохранённых данных.
5. Нормативная модель доступности
Нужно различать три понятия.
5.1. Plan contribution
Самостоятельный маркер и производные плана доступны только при выполнении
нынешнего контракта _planEntityAvailable() / _renderEntityAvailable().
Entity- или device-tombstone продолжает их выключать.
5.2. Explicit opening reference
opening.contact и opening.lock — точные ссылки на HA entity, а не ссылка на
маркер. Для них доступность определяется HA binding status точного
entity:<entity_id> и не зависит от tombstone маркера.
Ссылка доступна, только если resolveHaBindingStatus(...).kind === 'active':
| HA-ситуация | Кандидат | Сохранённая связь и runtime |
|---|---|---|
| Активная entity, маркер не удалён | да | работает |
Активная entity, удалён entity:* marker |
да | работает |
Активная entity, удалён parent device:* marker |
да | работает |
Активная registry entity со state unavailable/unknown |
да | сохраняется; показывается существующее неизвестное состояние |
| Точный live YAML entity без registry row | да | работает по действующему HA binding contract |
Entity или parent device имеет disabled_by |
нет | конфиг не стирается, runtime не действует |
| Authoritative registry подтверждает missing entity/parent | нет | конфиг не стирается, runtime не действует |
| Limited registry, есть точный live state | да | работает как подтверждённая active ссылка |
| Limited registry, остался authoritative cached disabled | нет | runtime не действует |
Limited registry без положительного свидетельства (unverified) |
нет | конфиг не стирается, runtime не действует |
unavailable здесь означает буквальное HA state существующей активной entity,
а не disabled_by. Для двери/окна и карточки используется уже существующее
представление unknown; outage не должен изображать ложное движение.
5.3. Render-frame availability
Отрисовка проёма читает только immutable active-registry projection текущего видимого кадра. Эта projection уже исключает HA-disabled и authoritative orphaned entries, но не применяет marker tombstone к явным ссылкам проёма.
Render-проверка должна требовать frozen state точной entity. Registry-less live
entity допустима, поскольку activeRegistryHass() уже считает точный live state
положительным свидетельством и сохраняет его в projection.
6. Контракт поведения
6.1. Выбор в Plan
- Контактный список по-прежнему содержит подходящие
binary_sensorи только door/window/opening/garage-likecover; текущая сортировка и friendly name не меняются. - Список замков по-прежнему содержит только
lock.*и сортируется по friendly name. - Активная подходящая entity присутствует в списке независимо от tombstone собственного маркера или parent device marker.
- Выбор записывает только
opening.contactлибоopening.lock. Tombstone, markers, layout и другие поля не меняются. - Один contact/lock можно выбрать у нескольких проёмов; изменения одного проёма не меняют остальные.
6.2. Уже сохранённая связь
Если contact/lock был выбран до удаления маркера, удаление маркера не меняет поле проёма и не выключает его. Если связь выбрана после удаления, результат тот же.
Повторное добавление самостоятельного маркера позже не создаёт вторую связь, не переписывает проём и не меняет его состояние: marker и opening reference — два независимых потребителя одной HA entity.
6.3. View и киоск
- contact управляет существующей анимацией и active tone только своего проёма;
unavailable,unknownи отсутствие frozen state используют существующий unknown/default визуальный контракт, не ложное открытие/закрытие;- lock показывает существующий locked/unlocked/unknown badge и строку в карточке проёма;
- явная кнопка карточки может вызвать lock/unlock только после повторной live проверки explicit opening reference;
- unlock по-прежнему требует confirmation, lock — нет;
- сам проём, маркер устройства и обычные controls не получают права управлять замком.
6.4. Удалённый маркер
Удалённый маркер остаётся удалённым и доступным для повторного добавления по
нынешнему контракту. Он не рисуется, не участвует в агрегатах, live text,
controls, Glow или других plan-level contributions. Исключение относится только
к точным полям opening.contact и opening.lock.
7. Архитектурный контракт реализации
Конкретные имена приватных методов могут отличаться, но граница должна быть явной и тестируемой.
- Ввести pure policy/helper для exact opening entity, который использует
resolveHaBindingStatus()и не принимает marker tombstones. - В full card разделить:
- live availability для picker и service action;
- render availability для frozen active projection.
- Перевести на новую политику только:
_contactCandidates();_lockCandidates();_openingAmt()и active tone_renderOpenings();_renderOpeningLocks();_renderOpeningInfoCard();_lockAction().
- Не менять
_planEntityAvailable()и_renderEntityAvailable()и не расширять список их plan-level consumers. - Не читать live
this.hass.statesв SVG-отрисовке вместо_renderPlanHass: atomic frame / reconnect continuity остаётся обязательной. - Candidate path не должен инициировать registry fetch на каждую entity; используется уже существующий shared snapshot и resolver.
Предполагаемые файлы реализации:
src/houseplan-card.ts;src/ha-binding-status.tsлибо небольшой отдельный pure module для policy;- unit-тест policy и матрицы статусов;
demo/smoke_opening_binding.mjsлибо эквивалентный узкий smoke;- документы и два changelog из раздела 12.
8. Модель данных, compatibility и миграция
Формат не меняется:
interface OpeningCfg {
contact?: string | null;
lock?: string | null;
}
- существующие exact entity IDs читаются без преобразования;
- tombstones не удаляются и не меняют форму;
- открытие/сохранение диалога без пользовательского изменения не должно очистить временно недоступную сохранённую ссылку;
- schema version, backend validation и import/export не меняются;
- прямой и обратной миграции нет.
9. UX, i18n и accessibility
Новых элементов интерфейса, текстов и i18n-ключей нет. Сохраняются текущие labels, сортировка, keyboard/native select semantics, dialog focus и a11y names.
На touch новый жест не вводится. View и киоск восстанавливают существующую
индикацию. Единственное действие замка остаётся крупной подписанной кнопкой
внутри открытой карточки; confirmation для unlock обязательно. Редактор Plan
остаётся desktop-first по docs/TOUCH-SUPPORT.md.
10. Критерии приёмки
- AC1 (
unit+smoke): активный подходящий contact и активный lock присутствуют в picker после удаления их exactentity:*marker. - AC2 (
unit+smoke): те же entity присутствуют и работают после удаленияdevice:*marker их parent device. - AC3 (
smoke): contact/lock, выбранные до удаления маркера, продолжают управлять анимацией, badge и карточкой проёма; выбранные после удаления дают тот же результат. - AC4 (
unit+smoke): выбор contact/lock не снимает tombstone, не создаёт marker/layout и не возвращает entity в LQI, климат, свет, Glow, live text или controls. - AC5 (
smoke): повторное добавление marker не дублирует и не изменяетopening.contact/opening.lock. - AC6 (
unit+smoke): одна entity может обслуживать несколько проёмов; изменение/очистка одного поля не меняет остальные. - AC7 (
unit+smoke): entity-disabled, parent-device-disabled, authoritative entity-missing и parent-missing не появляются как новые кандидаты и не выполняют runtime/action; сохранённые строки не стираются. - AC8 (
unit+smoke): active registry entity со stateunavailableилиunknownостаётся выбранной и показывает существующий unknown state без ложного движения и service call. - AC9 (
unit+smoke): live YAML entity и limited-registry entity с точным live state доступны;unverifiedи cached-disabled — недоступны. - AC10 (
smoke+ ревью кода): lock/unlock возможен только из карточки проёма; unlock требует confirmation; stale/disabled/orphaned lock не вызывает service. - AC11 (
unit+ ревью кода):_planEntityAvailable()и все потребители plan-level tombstone сохраняют прежнее поведение. - AC12 (
unit+build): typecheck, полный unit suite и production build зелёные; три bundle snapshot побайтно совпадают. - AC13 (ревью кода): пользовательская документация и RU/EN changelog точно описывают exception для explicit opening references и не обещают восстановления marker.
11. План автотестов
11.1. Unit
Pure matrix должна покрыть:
- active exact entity при отсутствии tombstone-контекста;
- entity-disabled, parent-disabled, entity-missing и parent-missing;
unavailable/unknownкак active binding с неизвестным state;- registry-less live YAML entity;
- limited live, limited unverified и cached disabled;
- render projection: exact frozen state принимается без требования marker;
- regression:
isRemovedPlanEntity()по-прежнему подавляет entity- и device-tombstone у обычных plan consumers.
11.2. Browser smoke
Один узкий сценарий на full card:
- создать два проёма с общим contact и lock;
- удалить exact entity marker, затем parent device marker;
- проверить options диалога, leaf/amount, active tone, padlock и info card;
- проверить выбор после удаления и отсутствие восстановленного marker;
- повторно добавить marker и убедиться, что opening fields не меняются;
- последовательно подать
on/off,locked/unlocked,unavailable; - подать disabled/orphaned registry rows и проверить отрицательные случаи;
- проверить единственный service call, confirmation unlock и отсутствие call у недоступного lock.
По текущему правилу владельца smoke добавляется при реализации, но запускается перед бетой; в обычном цикле реализации выполняются только typecheck, unit и build.
11.3. Golden и performance
Golden не нужен: стиль, геометрия и новый визуальный state не вводятся. Отдельный performance benchmark не нужен; beta проходит общий performance gate. Code review проверяет отсутствие per-entity registry fetch и повторного сканирования markers в render hot path.
12. Документация и release-артефакты
В том же user-visible commit обновить:
docs/CHANGELOG.ru.md;docs/CHANGELOG.md;docs/USER-GUIDE.ru.md— настройки проёма и точное исключение из раздела об удалённых маркерах;docs/FILTERING.md— plan contributions остаются выключенными, explicit opening references живут по HA binding status;docs/ARCHITECTURE.md— разделение marker availability и exact opening reference availability;docs/STATUS.md— уточнить shipped-контракт true plan deletion после фактической реализации.
Скриншоты и новые golden baselines не требуются. Отдельного security-артефакта нет; lock safety доказывается targeted smoke и независимым code review. Issue проходит beta/CI gate до стабильного релиза.
13. Производительность, безопасность и touch
- Производительность: новый helper O(1) поверх существующего binding-status resolver/snapshot; новых websocket запросов, subscriptions и render layers нет. Удаление marker scan из opening path не должно ухудшить budget.
- Безопасность: расширение намеренно возвращает сохранённому opening lock доступ к уже существующей явной кнопке. Live status перепроверяется перед service; unlock confirmation и запрет plan tap обязательны.
- Touch: View/киоск release-blocking; smoke проверяет, что badge открывает карточку, а сам проём остаётся inert. Plan picker — desktop-first.
14. Риски и снижение
| Риск | Вероятность / ущерб | Снижение |
|---|---|---|
| Случайно оживут live text/Glow/controls удалённого marker | средняя / высокий | новый узкий helper; старые plan helpers не менять; regression unit |
| Disabled или orphaned entity станет доступной | средняя / высокий | единый resolveHaBindingStatus; отрицательная unit/smoke matrix |
| Lock service пройдёт после registry change | низкая / критический | live re-check непосредственно перед confirmation/service |
| Render прочитает новый state поверх старого кадра | низкая / высокий | render только из immutable active projection |
| YAML entity ошибочно потребует registry row | средняя / средний | state-positive render policy и unit fixture |
| Диалог визуально очистит временно недоступную сохранённую ссылку | средняя / средний | lossless saved-value smoke; не мутировать до явного выбора |
| Повторное добавление marker перепишет проём | низкая / средний | независимые поля и re-add smoke |
15. Откат
Откат — revert единого behavior commit. Данные не мигрируются, поэтому старые
opening.contact, opening.lock и marker tombstones остаются валидными. После
отката вернётся прежняя ошибочная фильтрация, но конфигурация не потребует
восстановления. Feature flag и обратная миграция не нужны.
16. Принятые предположения — можно изменить без пересмотра продукта
- Новые подписи/предупреждения в picker не нужны: пользователь выбирает ту же HA entity, а независимость от marker объясняется документацией.
- Буквальный state
unavailable/unknownсчитается существующей active ссылкой и использует нынешнее unknown-представление; полное отсутствие live state не изображается как известное состояние. - Новый узкий smoke предпочтительнее расширения геометрических opening smoke; имя файла может измениться при сохранении того же покрытия.
- Блокирующих продуктовых вопросов перед ревью ТЗ нет.