mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
docs: specify area-less room device binding
Issue: #170 User-Visible: no
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
# Issue #170 — HA-устройство не привязывается к комнате без HA-зоны
|
||||
|
||||
- Дата: 2026-08-18
|
||||
- Тип: bug · приоритет P1 · ценность 8/10 · сложность/риск 4/10
|
||||
- Issue: [#170](https://github.com/Matysh/houseplan-card/issues/170)
|
||||
- Ветка: `issue/170-room-without-area`
|
||||
|
||||
Канонические документы: `docs/SCOPE.md`, `docs/ARCHITECTURE.md`,
|
||||
`docs/CONFIG-COMPATIBILITY.md`, `docs/TOUCH-SUPPORT.md`,
|
||||
`docs/USER-GUIDE.ru.md`, `docs/USER-GUIDE.md`.
|
||||
|
||||
## 1. Сценарий и персона
|
||||
|
||||
Администратор House Plan создаёт внутри пространства комнату без HA Area —
|
||||
например, гардеробную или кладовую. В редакторе устройств он назначает этой
|
||||
комнате уже существующее HA-устройство или отдельную HA-сущность и сохраняет
|
||||
маркер.
|
||||
|
||||
Это штатный сценарий J4/J6: модель плана может быть точнее модели Areas в Home
|
||||
Assistant, а её сопровождение не должно требовать создания искусственных зон.
|
||||
Корректная пространственная принадлежность также нужна J1: устройство должно
|
||||
отображаться там, куда его явно поместил пользователь.
|
||||
|
||||
## 2. Что человек увидит до и после
|
||||
|
||||
**До исправления:** выбор комнаты без HA-зоны сохраняется, но устройство после
|
||||
перестроения данных остаётся в пространстве своей HA Area либо исчезает из
|
||||
ожидаемого пространства. При повторном открытии редактора выбранная комната не
|
||||
восстанавливается корректно.
|
||||
|
||||
**После исправления:** явный выбор комнаты без HA-зоны имеет приоритет над HA
|
||||
Area устройства. Маркер сразу отображается в выбранном пространстве, а
|
||||
редактор при следующем открытии показывает ту же комнату. Уже сохранённые
|
||||
затронутые маркеры восстанавливаются автоматически, без повторного сохранения.
|
||||
|
||||
Внешний вид поля «Комната», маркера и плана не меняется.
|
||||
|
||||
## 3. Проблема и подтверждённая причина
|
||||
|
||||
Сохранение уже записывает корректный контракт:
|
||||
|
||||
- `marker.space` — пространство выбранной комнаты;
|
||||
- `marker.room_id` — id выбранной комнаты;
|
||||
- `marker.area: null` — у комнаты нет HA Area.
|
||||
|
||||
Ошибка возникает при чтении в общем `buildDevices()`. В ветках `device:*` и
|
||||
`entity:*` effective area вычисляется через `marker.area || registryArea`.
|
||||
Значимый `null` ошибочно принимается за отсутствие ручного выбора, после чего
|
||||
HA Area снова определяет и `DevItem.area`, и `DevItem.space`.
|
||||
|
||||
Подтверждённое воспроизведение на `origin/dev`: маркер сохранился как
|
||||
`{space: "garden", area: null, room_id: "<room>"}`, но runtime-проекция стала
|
||||
`{space: "f1", area: "living_room"}`. Повторное открытие редактора построило
|
||||
несуществующую пару `f1#@<room>`. Имеющийся `smoke_subarea` не заметил ошибку,
|
||||
поскольку проверяет только `binding: virtual`.
|
||||
|
||||
## 4. Scope
|
||||
|
||||
- исправить общую runtime-проекцию ручной комнаты без HA Area;
|
||||
- одинаково поддержать `device:*` и `entity:*`, включая entity с Area у
|
||||
родительского HA-устройства;
|
||||
- корректно отразить назначение в полном плане, `houseplan-space-card` и preview
|
||||
редактора через существующий общий `buildDevices()`;
|
||||
- восстановить точное выбранное значение при повторном открытии редактора;
|
||||
- сохранить действующие правила позиции маркера;
|
||||
- добавить unit- и browser-smoke-регрессии;
|
||||
- описать пользовательское исправление в обоих changelog.
|
||||
|
||||
## 5. Non-scope
|
||||
|
||||
- новый UI, новый способ создания комнат или изменение терминологии;
|
||||
- автоматическое создание/изменение HA Floors и Areas;
|
||||
- изменение схемы `markers[]`, версия конфигурации или миграция хранилища;
|
||||
- новая обработка удалённого/несуществующего `room_id`;
|
||||
- изменение автоматического размещения устройств без ручной комнаты;
|
||||
- изменение поведения виртуальных маркеров и комнат с HA Area;
|
||||
- новые room aggregates, источники температуры/влажности, LQI или правила Glow;
|
||||
- изменение визуала, touch-, accessibility- или performance-контракта.
|
||||
|
||||
## 6. Контракт поведения
|
||||
|
||||
### 6.1. Ручная комната без HA Area
|
||||
|
||||
Для явного HA-маркера `room_id` является признаком точного назначения в
|
||||
комнату. Если `room_id` — непустая строка и сохранённый `marker.area` равен
|
||||
`null`, runtime обязан получить:
|
||||
|
||||
- `DevItem.space` из сохранённого `marker.space`;
|
||||
- `DevItem.area === ""` как существующее runtime-представление отсутствующей
|
||||
HA Area;
|
||||
- неизменённый `marker.room_id`.
|
||||
|
||||
Registry Area устройства, сущности или её родительского устройства не должна
|
||||
переопределять эти значения. Правило одинаково для `device:*` и `entity:*`.
|
||||
|
||||
### 6.2. Остальные способы размещения
|
||||
|
||||
Если точного назначения в комнату без HA Area нет, сохраняется текущая
|
||||
семантика:
|
||||
|
||||
1. явная Area маркера определяет Area и пространство;
|
||||
2. для старого/metadata-only HA-маркера без явной комнаты применяется Area из
|
||||
entity/device registry;
|
||||
3. если Area не разрешается, применяется существующий fallback пространства;
|
||||
4. `virtual` продолжает использовать свою текущую ветку;
|
||||
5. удалённые, hidden, disabled, orphaned и unverified binding сохраняют
|
||||
действующие правила.
|
||||
|
||||
Исправление не должно превращать любое `area: null` в ручную комнату: точное
|
||||
правило активируется только при непустом `room_id`.
|
||||
|
||||
### 6.3. Редактор, preview и позиция
|
||||
|
||||
- Unsaved preview использует тот же production resolver и показывает маркер в
|
||||
выбранном пространстве без наследования registry Area.
|
||||
- После сохранения и повторного открытия поле «Комната» восстанавливает точное
|
||||
значение `<space>#@<room_id>`.
|
||||
- При смене комнаты внутри того же пространства существующая позиция маркера
|
||||
не меняется.
|
||||
- При переносе в другое пространство действует существующее центрирование по
|
||||
целевой комнате.
|
||||
|
||||
Новый отдельный resolver для полного или статического плана не допускается.
|
||||
|
||||
### 6.4. Пространственные потребители
|
||||
|
||||
Полный и статический планы получают одинаковые `space`, `area` и `room_id` из
|
||||
`buildDevices()`. Пустая effective area не должна возвращать устройство в
|
||||
агрегаты исходной HA Area. Существующий room-id-aware light/Glow resolver может
|
||||
использовать точную комнату; остальные агрегаты продолжают действовать по
|
||||
своим текущим Area/source-контрактам.
|
||||
|
||||
## 7. Данные, миграция и совместимость
|
||||
|
||||
Persisted-формат не меняется. Каноническая запись комнаты без HA Area остаётся
|
||||
`space: <id>`, `area: null`, `room_id: <id>`; runtime `area: ""` в `DevItem` не
|
||||
записывается обратно в конфигурацию.
|
||||
|
||||
Миграция не нужна: корректные значения уже находятся в существующих
|
||||
конфигурациях и должны начать читаться правильно сразу после обновления.
|
||||
Неизвестные sibling-поля маркера сохраняются обычным путём. Старые frontend
|
||||
после downgrade снова могут проявить исходный дефект, но данные при этом не
|
||||
требуют новой downgrade-конверсии.
|
||||
|
||||
Никаких изменений backend validation, WebSocket API, export/import envelope
|
||||
или `docs/CONFIG-COMPATIBILITY.md` не требуется.
|
||||
|
||||
## 8. i18n, accessibility и touch
|
||||
|
||||
Новых строк и переводов нет. Существующие label, focus order, keyboard и touch
|
||||
targets редактора не меняются. `docs/TOUCH-SUPPORT.md` остаётся без правок.
|
||||
|
||||
## 9. Acceptance criteria и доказательства
|
||||
|
||||
| AC | Критерий | Обязательное доказательство |
|
||||
|---|---|---|
|
||||
| AC1 | `device:*` с registry Area и сохранёнными `space`, `area:null`, `room_id` строится в сохранённом пространстве с `DevItem.area === ""`. | Targeted unit `buildDevices`. |
|
||||
| AC2 | То же правило работает для `entity:*`, даже если Area приходит от entity registry или родительского device. | Targeted unit с обоими registry-path. |
|
||||
| AC3 | Старый HA-маркер без `room_id`, ручная комната с HA Area, автоустройство и `virtual` сохраняют текущее размещение. | Негативные/regression unit cases. |
|
||||
| AC4 | Уже сохранённый маркер комнаты без HA Area корректно читается без resave или migration. | Unit-fixture существующей persisted-записи. |
|
||||
| AC5 | Сохранение реального HA-binding в комнату без Area переносит runtime-маркер в целевое пространство, а повторное открытие редактора восстанавливает точный выбор. | Расширенный `demo/smoke_subarea.mjs`. |
|
||||
| AC6 | Перенос между пространствами использует существующее центрирование, а выбор другой комнаты в том же пространстве не двигает расставленный маркер. | Browser-smoke с обеими ветками позиции. |
|
||||
| AC7 | Полный и статический планы используют единую исправленную проекцию без второго resolver. | Unit общего `buildDevices` и code review shared call sites. |
|
||||
| AC8 | Изменение проходит рабочие implementation-gates. | `npm run typecheck`, `npm run test:unit`, `npm run build`; targeted browser smoke до code review. |
|
||||
|
||||
## 10. План автотестов
|
||||
|
||||
### 10.1. Unit
|
||||
|
||||
В `test/devices.test.mjs` добавить минимальные fixtures для:
|
||||
|
||||
- `device:*`: registry Area конфликтует с ручной area-less комнатой;
|
||||
- `entity:*`: конфликт с Area самой entity и с Area parent device;
|
||||
- сохранённой до исправления записи без миграции;
|
||||
- маркера без `room_id`, который обязан продолжить registry fallback;
|
||||
- комнаты с HA Area, автоустройства и virtual marker.
|
||||
|
||||
Проверять одновременно `space`, `area` и сохранение `marker.room_id`, чтобы
|
||||
частичное исправление не прошло незамеченным.
|
||||
|
||||
### 10.2. Browser smoke
|
||||
|
||||
Расширить `demo/smoke_subarea.mjs`, не удаляя существующий virtual-сценарий:
|
||||
|
||||
1. открыть реальный HA device/entity, исходно принадлежащий Area другого
|
||||
пространства;
|
||||
2. выбрать комнату без Area и сохранить;
|
||||
3. проверить persisted `space/area/room_id` и runtime `space/area`;
|
||||
4. повторно открыть редактор и проверить точное значение выбора;
|
||||
5. доказать центрирование при межпространственном переносе;
|
||||
6. доказать сохранение позиции при смене комнаты внутри пространства.
|
||||
|
||||
Golden/screenshot не нужен: пиксельный контракт не меняется. Полный smoke,
|
||||
golden и performance остаются общими пред-бета gates по runbook.
|
||||
|
||||
### 10.3. Executable mutation gate
|
||||
|
||||
Тесты обязаны падать хотя бы при следующих намеренных мутациях:
|
||||
|
||||
- возврат `marker.area || registryArea` в device-ветке;
|
||||
- исправление только `device:*`, без `entity:*`;
|
||||
- применение area-less-семантики ко всем `area:null`, без проверки `room_id`;
|
||||
- построение reopened room через registry-derived space;
|
||||
- центрирование уже расставленного маркера при смене комнаты в том же
|
||||
пространстве.
|
||||
|
||||
## 11. Риски и меры
|
||||
|
||||
| Риск | Мера |
|
||||
|---|---|
|
||||
| Сломан registry fallback старых явных маркеров без ручной комнаты. | Отдельный negative unit без `room_id`. |
|
||||
| Исправлена device-, но не entity-ветка. | Симметричные AC1/AC2 и mutation gate. |
|
||||
| Пространство исправлено, но registry Area продолжает влиять на room aggregates. | В AC1/AC2 обязательно проверять и `area === ""`. |
|
||||
| Полный и статический планы расходятся. | Исправлять общий `buildDevices`, не добавлять renderer-specific resolver. |
|
||||
| Исправление случайно меняет позицию существующего маркера. | Две browser-проверки same-space/cross-space. |
|
||||
| Начинается неоговорённая очистка stale `room_id`. | Stale/deleted room явно оставлен в non-scope. |
|
||||
|
||||
Performance-риск пренебрежимо мал: добавляется только константная проверка
|
||||
полей на явный marker. Новых DOM-узлов, таймеров, подписок, сетевых запросов и
|
||||
persisted writes нет. Security/privacy boundary не меняется.
|
||||
|
||||
## 12. Rollback
|
||||
|
||||
Исправление можно откатить одним frontend-коммитом: persisted-схема и backend
|
||||
не меняются. Откат возвращает исходный баг чтения, но не требует восстановления
|
||||
данных. Перед откатом оставить новые тесты как описание известного контракта
|
||||
либо явно откатить их вместе с поведением; частичный откат resolver без тестов
|
||||
не допускается.
|
||||
|
||||
## 13. Release-артефакты
|
||||
|
||||
- пользовательские записи в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том
|
||||
же implementation-коммите (`User-Visible: yes`);
|
||||
- `docs/USER-GUIDE.ru.md` и `docs/USER-GUIDE.md` уже описывают требуемое
|
||||
поведение, поэтому смысловая правка не нужна; при реализации проверить, что
|
||||
формулировки не устарели;
|
||||
- обновлённые unit и `demo/smoke_subarea.mjs`;
|
||||
- синхронные build-артефакты по действующему D-контракту;
|
||||
- screenshots/golden не требуются;
|
||||
- отдельные performance/security artifacts не требуются;
|
||||
- перед бетой выполняются общие golden, smoke и performance gates; Linux CI
|
||||
остаётся каноном полного HA harness.
|
||||
|
||||
## 14. Принятые предположения
|
||||
|
||||
1. Непустой `room_id` вместе с `area:null` однозначно означает намеренное
|
||||
назначение в комнату без HA Area; это уже записывает текущий редактор.
|
||||
2. `marker.space` у такой сохранённой записи валиден. Если он отсутствует или
|
||||
указывает на удалённое пространство, применяется текущий безопасный fallback
|
||||
без новой диагностики или автоматической очистки.
|
||||
3. Текущая политика позиции — сохранить координаты внутри того же пространства
|
||||
и центрировать только при смене пространства — является продуктовым
|
||||
контрактом и не пересматривается этой задачей.
|
||||
4. Исправление должно восстанавливать уже сохранённые записи только чтением;
|
||||
фоновая миграция и принудительный resave не допускаются.
|
||||
5. Вопросов владельцу нет: ожидаемое поведение уже закреплено в issue и
|
||||
пользовательском руководстве.
|
||||
@@ -52,6 +52,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
|
||||
| [#164](https://github.com/Matysh/houseplan-card/issues/164) Активный цикл стиральной машины должен быть жёлтым | [164-washer-active-cycle.md](164-washer-active-cycle.md) |
|
||||
| [#166](https://github.com/Matysh/houseplan-card/issues/166) Солнечные лучи зеркально учитывают направление севера | [166-sun-north-rotation.md](166-sun-north-rotation.md) |
|
||||
| [#167](https://github.com/Matysh/houseplan-card/issues/167) Экспорт «только планировка» | [167-plan-only-export.md](167-plan-only-export.md) |
|
||||
| [#170](https://github.com/Matysh/houseplan-card/issues/170) HA-устройство не привязывается к комнате без HA-зоны | [170-room-without-area.md](170-room-without-area.md) |
|
||||
|
||||
## P2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user