Files
houseplan-card/docs/specs/020-glow-open-door-spill.md
2026-08-28 22:31:02 +03:00

17 KiB
Raw Permalink Blame History

ТЗ #20 — Glow проходит через дверь по её фактическому состоянию

Сценарий

Домочадец или гость смотрит House Plan в обычном View либо на киоск-панели в момент, когда в одной комнате горит свет, а дверь с привязанным контактным датчиком между комнатами открывается или закрывается.

Что человек увидит до и после

Сейчас свет проходит через такую дверь независимо от её состояния; после изменения закрытая дверь остановит Glow, открытая пропустит его, а частично открытые ворота или cover оставят проход соответствующей ширины.

Проблема

Каноническая модель docs/LIGHT.md строит полигон видимости по реальной кладке: дверь, ворота или сохранённый passage вырезаются из стены и поэтому всегда прозрачны для Glow. _lightBarriers() классифицирует тип и наличие пола по обе стороны, но не учитывает состояние привязанного opening.contact. В результате визуальный символ может показывать закрытую дверь, а свет продолжает проходить через полный проём.

Старое описание задачи упоминало doorSector() и отдельные световые секторы. Такой модели после #71 нет: задача меняет только ширину выреза в той же кладке, которая уже является источником барьеров и fail-dark проверки источника.

Скоуп

  • двери и ворота, привязанные к доступному binary_sensor или door-like cover;
  • полный, нулевой и частичный коэффициент открытия;
  • проёмы в контурных стенах и в независимых перегородках;
  • единый коэффициент для символа проёма и апертуры 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. Реальная апертура в кладке

  1. Сначала действует существующая allowlist door | gate | passage и проверка пола по обе стороны. Окно, неизвестный будущий тип и наружный проём остаются непрозрачными при любом состоянии.
  2. Для прошедшего классификацию проёма вычисляется amount.
  3. При amount <= 0 вырез не создаётся: исходная кладка остаётся целой и останавливает свет.
  4. При amount >= 1 используется существующий полный вырез без изменения.
  5. При 0 < amount < 1 длина выреза равна renderedLength * amount, а его центр, ось и глубина стены не меняются.
  6. Правило одинаково для контурной стены и независимой перегородки. Для перегородки масштабируется именно PartitionOpeningCut, а не сохранённый OpeningCfg и не видимая геометрия стены.
  7. Несколько проёмов обрабатываются действующей 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факт.

План автотестов

  1. Расширить test/logic.test.mjs: binary, cover position 0/1/50/100, clamp, invalid value, invert, outage fallback, passage/window.
  2. Вынести чистую подготовку light apertures/signature либо покрыть существующий чистый geometry boundary так, чтобы unit проверял длину, центрирование, порядок и partition cut без Lit/DOM.
  3. Расширить demo/smoke_glow.mjs динамической сменой contact и измерением пикселей в A, в апертуре, в B, за jamb и снаружи. Тот же smoke проверяет контурную стену и перегородку.
  4. Добавить/обновить одну детерминированную golden-сцену closed/open/50%; baseline принимается только из полного Linux CI arteфакта по процессу.
  5. Перед 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.
  6. Перед бетой: 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 сценариев по файлам.