17 KiB
ТЗ #20 — Glow проходит через дверь по её фактическому состоянию
- Issue: https://github.com/Matysh/houseplan-card/issues/20
- Каноническая модель света:
docs/LIGHT.md - Связано: #19, #55, #71, #92, #306
Сценарий
Домочадец или гость смотрит House Plan в обычном View либо на киоск-панели в момент, когда в одной комнате горит свет, а дверь с привязанным контактным датчиком между комнатами открывается или закрывается.
Что человек увидит до и после
Сейчас свет проходит через такую дверь независимо от её состояния; после изменения закрытая дверь остановит Glow, открытая пропустит его, а частично открытые ворота или cover оставят проход соответствующей ширины.
Проблема
Каноническая модель docs/LIGHT.md строит полигон видимости по реальной кладке:
дверь, ворота или сохранённый passage вырезаются из стены и поэтому всегда
прозрачны для Glow. _lightBarriers() классифицирует тип и наличие пола по обе
стороны, но не учитывает состояние привязанного opening.contact. В результате
визуальный символ может показывать закрытую дверь, а свет продолжает проходить
через полный проём.
Старое описание задачи упоминало doorSector() и отдельные световые секторы.
Такой модели после #71 нет: задача меняет только ширину выреза в той же кладке,
которая уже является источником барьеров и fail-dark проверки источника.
Скоуп
- двери и ворота, привязанные к доступному
binary_sensorили door-likecover; - полный, нулевой и частичный коэффициент открытия;
- проёмы в контурных стенах и в независимых перегородках;
- единый коэффициент для символа проёма и апертуры Glow;
- инвалидация barrier/region cache только при изменении релевантного состояния;
- View и kiosk; та же геометрия участвует в существующих golden/smoke сценах.
Не-скоуп
- новая настройка, новый элемент интерфейса или сохранение runtime-состояния;
- изменение конфигурационной схемы и миграция;
- солнечные лучи, room fill, площадь, бумага, tunnel fill и геометрия символа кроме уже поддерживаемого коэффициента открытия;
- попытка угадать механику створки: частичная апертура всегда сужается симметрично относительно центра;
- свет за наружной дверью или воротами;
- изменение fallback переходных состояний cover без числовой позиции;
- автоматическое управление дверью или связанной сущностью.
Контракт поведения
1. Один коэффициент открытия
Канонический openingAmount() остаётся единственным источником числа 0..1,
которое используют и видимый символ, и Glow. Resolver получает тип проёма,
состояние, invert и при наличии — attributes.current_position связанного
cover.
| Проём / источник | Коэффициент amount |
|---|---|
passage |
всегда 1 |
| дверь/ворота без contact | 1 — действующая семантика постоянного проёма |
| доступный бинарный contact, закрыто/неактивно | 0 |
| доступный бинарный contact, открыто/активно | 1 |
доступный cover с конечным current_position |
clamp(position / 100, 0, 1) |
известное значение при invert: true |
1 - amount |
contact missing/disabled либо unknown/unavailable |
fallback непривязанной двери/ворот: 1; invert его не меняет |
| окно | его символ использует resolver как сейчас, но апертура Glow всегда 0 |
Для cover без конечного current_position сохраняется действующая интерпретация
его строкового state. Задача не вводит last-known кеш и не меняет существующее
поведение opening/closing без позиции. Значения NaN, бесконечность и
нечисловые строки считаются отсутствующей позицией.
2. Реальная апертура в кладке
- Сначала действует существующая allowlist
door | gate | passageи проверка пола по обе стороны. Окно, неизвестный будущий тип и наружный проём остаются непрозрачными при любом состоянии. - Для прошедшего классификацию проёма вычисляется
amount. - При
amount <= 0вырез не создаётся: исходная кладка остаётся целой и останавливает свет. - При
amount >= 1используется существующий полный вырез без изменения. - При
0 < amount < 1длина выреза равнаrenderedLength * amount, а его центр, ось и глубина стены не меняются. - Правило одинаково для контурной стены и независимой перегородки. Для
перегородки масштабируется именно
PartitionOpeningCut, а не сохранённыйOpeningCfgи не видимая геометрия стены. - Несколько проёмов обрабатываются действующей boolean-геометрией. Порядок записей в конфиге не меняет результат и не создаёт швов между пересекающимися вырезами.
Источник, поставленный внутрь закрывшегося проёма, попадает в непрозрачную кладку и полностью подавляется существующим fail-dark guard — так же, как источник внутри стены. Отдельного исключения для такого источника нет.
3. Обновление и кеши
Статическая геометрия и HA-состояние разделяются:
geometryFingerprintпо-прежнему считается только из пространства,cell_cmи grid pitch и используется для повторного recut общей стеновой геометрии;openingStateSignatureсодержит отсортированные по id записи только интерьерных дверей/ворот с привязкой:id:amount, где amount округлён до0.001;barrierFingerprintвключаетgeometryFingerprintиopeningStateSignature; он ключует_lightBarrierPoolи возвращается в region cache;- кеш физических тел независимых стен учитывает фактические частичные cuts, а не только множество id прозрачных проёмов;
- посторонний HA tick с той же signature обязан попадать в кеш; глобальная очистка на каждый update запрещена.
Изменение contact должно попасть в обычный render tick без сохранения config. Пулы остаются ограниченными действующими LRU-лимитами.
UX
Новых контролов и сообщений нет. Пользователь продолжает привязывать contact в существующем диалоге проёма. Результат виден непосредственно на плане:
- закрытие убирает Glow за дверью;
- открытие возвращает его;
- позиционный cover меняет ширину светового прохода вместе с отображаемой степенью открытия.
Недоступность или деактивация contact не превращает архитектурный проём в стену и не порождает повторяющиеся toast/ошибки.
Модель данных и миграция
Модель и сохранённый JSON не меняются. Используются существующие поля
opening.contact и opening.invert и live-state Home Assistant. Старые планы
не мигрируются и при отсутствии доступной привязки отображаются как раньше.
i18n
Новых строк нет. Существующие названия состояния проёма и contact не меняются.
Критерии приёмки и доказательства
- AC1. Лампа в комнате A и закрытая привязанная дверь в B: B не получает
Glow. Доказательство: unit геометрии +
demo/smoke_glow.mjs. - AC2. После перехода того же contact в открытое состояние Glow появляется
в B не позднее следующего render tick, без config save. Доказательство:
browser smoke с переключением fake
hass. - AC3.
cover.current_position = 50создаёт центрированный вырез ровно в половину полной длины, а символ получает тот жеamount. Доказательство: unit единого resolver и geometry input + smoke-пиксели по обе стороны jamb. - AC4.
invertменяет известное бинарное/позиционное значение, но не меняет fallback missing/disabled/unknown/unavailable. Доказательство: unit-матрица. - AC5. Ворота повторяют дверь; passage всегда прозрачен; окно, неизвестный тип и наружная дверь/ворота всегда непрозрачны для Glow. Доказательство: unit классификатора + расширенный glow smoke.
- AC6. Те же full/zero/partial правила работают для проёма с
host.kind = partition; закрытая перегородка и её jamb остаются барьерами. Доказательство: geometry unit + smoke fixture с независимой перегородкой. - AC7. Закрытие проёма, внутри которого стоит источник, полностью убирает его Glow. Доказательство: source-guard unit/smoke.
- AC8. Одинаковая signature переиспользует barrier cache при постороннем HA update; изменение amount создаёт новый barrier/region key; LRU остаётся bounded. Доказательство: cache contract unit.
- AC9. На large-house fixture переключение 20 door contacts не нарушает
действующий
stateUpdate/Glow performance budget. Доказательство: exact-SHA CI performance smoke перед бетой. - AC10. Golden-сцена показывает closed/open/50% на толстой стене без тёмных швов и без света снаружи. Доказательство: reviewed Linux golden arteфакт.
План автотестов
- Расширить
test/logic.test.mjs: binary, cover position0/1/50/100, clamp, invalid value, invert, outage fallback, passage/window. - Вынести чистую подготовку light apertures/signature либо покрыть существующий чистый geometry boundary так, чтобы unit проверял длину, центрирование, порядок и partition cut без Lit/DOM.
- Расширить
demo/smoke_glow.mjsдинамической сменой contact и измерением пикселей в A, в апертуре, в B, за jamb и снаружи. Тот же smoke проверяет контурную стену и перегородку. - Добавить/обновить одну детерминированную golden-сцену closed/open/50%; baseline принимается только из полного Linux CI arteфакта по процессу.
- Перед
S7-code-review:npm run typecheck,npm test,npm run build,npm run bundle:sync,npm run bundle:budget,node demo/smoke_glow.mjs,node demo/docs/capture.mjs, затем проверка синхронности bundle tree. - Перед бетой: exact-SHA Validate с golden, полным browser matrix и performance smoke.
Риски и меры
| Риск | Мера |
|---|---|
| stale Glow после изменения contact | state signature входит и в barriers, и в region fingerprint |
| частичный проём режется в контуре, но не в перегородке | отдельный AC6 и общий чистый aperture input |
| HA update пересчитывает дорогую boolean-геометрию без нужды | signature содержит только релевантные id/amount, LRU остаётся bounded |
| источник внутри закрытой двери протекает на одну сторону | AC7 фиксирует общий fail-dark body |
| визуал и свет используют разные проценты | один openingAmount() и unit single-source contract |
| partial cut создаёт микрошов или вылезает за jamb | clamp, центрирование, существующая boolean-геометрия и golden |
Откат
Изменение не пишет данные. Безопасный откат — вернуть полную апертуру для всех интерьерных door/gate/passage и удалить state signature из barrier cache. Планы, привязки и сохранённые trails при этом не меняются.
Release-артефакты
- записи RU/EN changelog со ссылкой на #20;
- актуализация
docs/LIGHT.mdи при необходимости архитектурного описания кеша; - синхронные production/demo bundle trees;
- reviewed golden baseline, если пиксели закономерно изменились;
- в release body — отдельный пункт только если владелец сочтёт эффект значимым,
иначе общий
Small fixes and improvements.
Принято предположительно, поменять свободно на ревью
- форма чистого aperture helper и место его файла;
- точная сериализация signature при сохранении семантики и округления
0.001; - способ построения частичного
PartitionOpeningCutбез мутации config; - разбиение smoke/golden сценариев по файлам.