31 KiB
Issue #104 — HA-привязка проёма после удаления маркера
- Issue: https://github.com/Matysh/houseplan-card/issues/104
- Статус: редакция r2 после ревью r1; решения владельца зафиксированы, готово к повторному независимому ревью
- Тип / приоритет: bug / P2
- Оценка: пользовательская ценность 8/10; сложность и риск 5/10
- Область: настройки проёма в Plan, состояние проёма и замка в View/киоске, политика доступности HA-привязок, документация и тесты
- Модель данных: без изменений и миграции
- Связано: #98, #117,
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.
В full card метод _captureRenderDeviceSnapshot() явно добавляет contact и
lock каждого проёма в общий entityIds, после чего generic
createRenderDeviceSnapshot() замораживает запрошенную HA-проекцию. Сам модуль
render-device-snapshot.ts ничего не знает о проёмах. Поэтому основной дефект
на registry-backed пути находится в политике доступности, а не в формате
конфигурации или списке entity IDs, переданном snapshot builder.
2.1. Конфликт с действующим письменным контрактом
docs/FILTERING.md сейчас объединяет три разных вида сохранённых ссылок одним
правилом: ссылки в проёмах, live text и marker.controls[] сохраняются, но
становятся неактивными после удаления соответствующего marker; повторное
добавление binding оживляет их.
#104 намеренно отменяет эту норму как минимум для проёмов. Это не простое исправление расхождения кода и документа: это частичная замена записанного продуктового решения. Новая формулировка должна описать все три случая, даже если меняется только один.
Принятая владельцем новая формулировка:
- exact
opening.contactиopening.lockживут по HA binding status и не выключаются tombstone самостоятельного marker; - live text и
marker.controls[]по-прежнему сохраняются, но не действуют, пока tombstone существует; повторное добавление binding оживляет их; - HA-disabled и authoritative orphaned entity не действует ни в одном из трёх случаев; конфигурация при этом не стирается.
Новый принцип не распространяется на live text или controls[]: это явное
решение scope #104, а не случайный пробел реализации.
3. Скоуп
В задачу входят:
- отдельная политика доступности явной HA-ссылки проёма;
- списки контактных датчиков и замков в свойствах проёма;
- анимация/тон двери, окна или ворот по контактному датчику;
- бейдж замка, карточка проёма и единственная разрешённая кнопка lock/unlock;
- случаи entity-tombstone и device-tombstone;
- сохранение текущей семантики HA-disabled, orphaned, limited registry и
unavailablestate; - unit-тесты, целевой browser smoke и актуализация пользовательского контракта.
4. Не входит в задачу
- восстановление, повторное создание или перемещение удалённого маркера;
- изменение
removedtombstone, списка Добавить или повторного добавления; - ослабление фильтрации LQI, температуры, влажности, света, Glow, vacuum и других plan-level contributions;
- изменение tombstone-семантики live text и
marker.controls[]— решением владельца они сохраняют нынешнее отключение до повторного добавления binding; - новый 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 | да | picker уже принимает; render-расхождение вынесено в #117 и не меняется в #104 |
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 к явным ссылкам проёма.
Сегодня _renderEntityAvailable() требует одновременно registry entity row и
frozen state. activeRegistryHass() сохраняет state registry-less live entity,
но не синтезирует для неё строку в entities; поэтому picker такую entity уже
принимает, а render — нет. Это отдельное текущее расхождение, а не сохранённое
поведение #104.
Решение владельца: не расширять #104. Новая opening render-проверка игнорирует marker tombstone, но сохраняет требование frozen active registry row и state. Исправление registry-less render вынесено в #117.
5.4. Решения владельца
Владелец принял все defaults 13.08.2026; каноническая запись: https://github.com/Matysh/houseplan-card/issues/104#issuecomment-5278421151
- Q1: новый принцип действует только для openings; live text и
controls[]не меняются; - Q2: CR-1 в
docs/SCOPE.mdне меняется, потому что поверхность актуации остаётся той же; - Q3: registry-less render исключён из #104 и отслеживается в #117.
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 только своего проёма;
- то же значение amount участвует в существующем hit-test проёма через
_openingAt(); смена availability не должна разъединить видимую и кликабельную геометрию; 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 с сохранением требования active registry row + state; registry-less расхождение относится к #117.
- Перевести на новую политику только:
_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 (
unit+smoke): pure policy даёт одинаковый результат независимо от того, была exact opening reference сохранена до или после marker tombstone; browser smoke подтверждает анимацию, badge и карточку проёма. - AC4 (
unit+smoke): выбор contact/lock не снимает tombstone, не создаёт marker/layout и не возвращает entity в LQI, климат, свет, Glow, live text или controls. - AC5 (
unit+smoke): операции удаления/повторного добавления marker в pure config fixture не дублируют и не изменяютopening.contact/opening.lock; browser smoke подтверждает интеграцию диалога. - 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): limited-registry entity с точным live state доступна;unverifiedи cached-disabled — недоступны. Существующий picker продолжает принимать live YAML entity, а её render-расхождение в #104 не меняется и отслеживается в #117. - AC10 (
unit+ source-contract +smoke+ ревью кода): lock/unlock возможен только из карточки проёма; availability guard стоит до confirmation/service,callService('lock', …)не появляется на другой plan-поверхности, unlock требует confirmation, а stale/disabled/orphaned lock не вызывает service. Unit и source-contract выполняются до code review; browser smoke повторяет пользовательский путь перед бетой. - 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 остаётся active в exact binding resolver picker; render requirement не меняется и отслеживается в #117;
- limited live, limited unverified и cached disabled;
- render projection: exact frozen registry row + state принимаются без требования marker, но state-only registry-less путь не расширяется;
- regression:
isRemovedPlanEntity()по-прежнему подавляет entity- и device-tombstone у обычных plan consumers; - pure config fixture: marker delete/re-add не меняет opening fields и не связывает несколько проёмов друг с другом;
- lock action policy: active проходит, disabled/orphaned/unverified не достигают service intent.
11.2. Source-contract для lock safety
До code review отдельный Node test читает src/houseplan-card.ts через уже
применяемый в suite приём methodBody() и доказывает:
_lockAction()вызывает exact opening availability guard до confirmation иcallService;- unlock confirmation остаётся внутри санкционированного метода;
callService('lock', ...)не появляется в другом plan interaction path;- сам opening hit и device marker не получают lock actuation.
11.3. 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.4. 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 после фактической реализации.
docs/SCOPE.md и lock invariant CR-1 проверены обязательно. Решением владельца
текст не меняется: санкционированная поверхность остаётся той же единственной
кнопкой карточки проёма, меняется лишь availability exact lock.
Скриншоты и новые 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; имя файла может измениться при сохранении того же покрытия.
- Решения Q1–Q3 зафиксированы владельцем в §5.4 и не относятся к изменяемым предположениям этого раздела.