Files
houseplan-card/docs/superpowers/specs/2026-08-08-always-static-device-icon-design.md
T
Matysh 2219700d63
Validate / hacs (push) Failing after 12s
Validate / hassfest (push) Failing after 12s
Validate / frontend (push) Successful in 3m58s
Validate / backend (push) Failing after 8m11s
Validate / smoke (push) Failing after 22m39s
Release v1.60.2
2026-08-08 17:39:25 +03:00

38 KiB
Raw Blame History

Всегда статичный значок устройства

Статус: ТЗ актуализировано по ревью, реализовано и проверено в v1.60.2 Дата: 2026-08-08 Область: редактор устройств, все поверхности отображения marker, единый resolver визуального состояния, backend-валидация и пользовательская документация

1. Краткое продуктовое решение

В поле «Отображение» добавляется четвёртый режим:

«Всегда статичный значок».

В этом режиме видимый marker устройства всегда имеет один и тот же нейтральный вид:

  • стандартная тёмная подложка marker;
  • базовая выбранная или автоматически определённая иконка;
  • заданные пользователем размер и угол поворота;
  • без изменения подложки, прозрачности, цвета и иконки из-за состояния Home Assistant;
  • без activity/pulse-эффекта;
  • без RGB-окраски от источника света;
  • без подмены иконки по состоянию;
  • без живых дополнительных значений температуры, влажности и качества связи.
  • без отдельного движущегося puck, следа и других state-driven overlays живого пылесоса.

Режим является только способом отображения marker. Он не изменяет привязку, действия по нажатию, принадлежность комнате, участие устройства в управлении, источниках света, Glow, заливке комнаты или расчётах плана.

Текущий режим «Значок» переименовывается в:

  • RU: «Значок + динамическая подложка»;
  • EN: «Icon + dynamic plate».

Его внутренняя модель и текущее поведение не меняются. Он остаётся режимом по умолчанию.

2. Причина изменения

Сейчас минимальный режим badge, показанный пользователю как «Значок», всё равно является динамическим:

  • работающие устройства получают жёлтую подложку;
  • открытые объекты получают отдельное состояние;
  • тревоги становятся красными;
  • off, unknown и unavailable используют приглушённое представление согласно действующей общей логике;
  • некоторые классы устройств меняют саму иконку по состоянию;
  • активный источник света может передавать marker цвет;
  • при включённых общих настройках рядом появляются температура, влажность или LQI.

Название «Значок» не объясняет эту динамику, а отдельного способа намеренно оставить устройство визуально нейтральным нет. Это особенно заметно у устройств с большим числом вспомогательных сущностей, у декоративных marker и у объектов, состояние которых не нужно акцентировать на плане.

3. Цели

  1. Дать пользователю предсказуемый режим, в котором HA state не меняет вид marker.
  2. Явно назвать существующий режим badge динамическим.
  3. Сохранить текущий режим по умолчанию для новых и существующих устройств.
  4. Обеспечить одинаковый результат на интерактивном плане, в карточке пространства и в предпросмотре настроек.
  5. Не смешивать отображение marker с логикой света, управления и данных плана.
  6. Не требовать миграции существующих конфигураций.

4. Не входит в задачу

  • изменение правил, по которым определяется состояние устройства;
  • изменение resolvedLightSources(room);
  • изменение Glow, заливки «Свет» или управления источниками света;
  • изменение tap action, more-info или диалога устройства;
  • создание отдельной пользовательской палитры статичного marker;
  • фиксация автоматически выбранной MDI-иконки как ручного override;
  • изменение правил скрытых, удалённых и деактивированных в HA устройств;
  • изменение поведения остальных трёх режимов, кроме переименования badge в UI.

Touch editor: best effort / intentionally degraded согласно docs/TOUCH-SUPPORT.md. Native select обязан оставаться сохранённым и работоспособным на узком экране, но мобильная компоновка редактора не является отдельной feature-parity целью. View и kiosk полностью поддерживаются.

5. Список режимов и порядок в UI

Порядок в выпадающем списке обязателен:

№ Persisted value RU EN Краткое поведение
1 badge или null Значок + динамическая подложка Icon + dynamic plate Иконка и текущая динамическая подложка
2 icon_ripple Значок + активность Icon + activity Динамическая подложка плюс activity-эффект
3 value Значение вместо иконки Value instead of an icon Значение, когда его можно однозначно получить, иначе иконка
4 static_icon Всегда статичный значок Always static icon Всегда нейтральная подложка и базовая иконка

Legacy-значение ripple по-прежнему принимается только для чтения и нормализуется в icon_ripple; в UI оно не показывается.

6. Значение по умолчанию и совместимость

6.1. Новые устройства

Новый marker по умолчанию получает UI-режим badge. При сохранении значение может, как и сейчас, быть записано как null/отсутствующее поле. Это означает «Значок + динамическая подложка», а не статичный значок.

6.2. Существующие устройства

  • отсутствующий display → badge;
  • display: null → badge;
  • display: "badge" → badge;
  • display: "ripple" → icon_ripple;
  • display: "icon_ripple" → icon_ripple;
  • display: "value" → value;
  • display: "static_icon" → новый статичный режим;
  • неизвестное значение во frontend должно безопасно давать badge, а backend продолжает отклонять его при записи.

Автоматически переводить существующие устройства в static_icon запрещено.

6.3. Изменение режима

Переключение режима в открытом диалоге сразу обновляет предпросмотр, но не меняет server config до нажатия «Сохранить». Отмена диалога возвращает сохранённый режим.

7. Нормативный визуальный контракт static_icon

7.1. Что остаётся видимым

  • базовая иконка d.icon;
  • ручная иконка, если пользователь её выбрал;
  • автоматически определённая иконка, если ручного override нет;
  • пользовательский размер marker;
  • пользовательский угол поворота;
  • обычная рамка и тень нейтрального marker согласно теме House Plan.

«Чёрная подложка» в требованиях означает текущую стандартную нейтральную подложку marker, а не жёстко заданный #000000. Реализация обязана использовать действующие theme tokens (--hp-bg, --hp-line, --hp-txt или их актуальные преемники), чтобы сохранить контраст и совместимость с темами HA.

7.2. Что принудительно отключается

Для static_icon renderer-ready presentation всегда получает:

  • visual.availability = "available";
  • visual.status = "neutral";
  • visual.activity = "none";
  • activity = "none";
  • valueText = null;
  • valueFullText = null;
  • fallbackReason = null;
  • tempText = null;
  • humText = null;
  • lqiText = null;
  • lqiColor = null;
  • lightColor = null;
  • базовую, а не state-morphed иконку;
  • отсутствие классов on, open, alarm, unavail и activity-*.

Отдельный live-vacuum renderer также обязан скрыть движущийся puck, текущий/предыдущий след и room-highlight для этого marker. Runtime/серверная запись следа может продолжаться независимо: смена режима отображения не удаляет историю уборки.

Ручные ripple_color и ripple_size, оставшиеся в старой конфигурации, не используются и не показываются в форме, пока выбран static_icon. Их сохранённые значения не должны неожиданно влиять на статичный marker.

7.3. Состояния, которые не меняют marker

Условие Результат в static_icon
Устройство включено Нейтральный marker
Устройство выключено Тот же нейтральный marker
Устройство работает Тот же нейтральный marker
Устройство остановлено Тот же нейтральный marker
Движение/присутствие обнаружено Тот же нейтральный marker
Cover открывается/закрывается Тот же нейтральный marker и базовая иконка
Дверь/замок открыт Тот же нейтральный marker
Сущность unknown/unavailable Тот же нейтральный marker, без fade
Тревога/протечка/дым Тот же нейтральный marker, без красной подложки
Свет включён Тот же нейтральный marker, без жёлтой/RGB-подложки
Краткое event-событие Тот же marker, без activity ring
Общая настройка live states включена/выключена Тот же marker
Включены temperature/LQI badges Для этого marker badges не показываются
Живой пылесос движется или имеет сохранённый след Puck, след и room-highlight не рисуются

Подавление тревоги является осознанным следствием режима. Предпросмотр и диалог Home Assistant всё ещё могут показывать фактическое тревожное состояние, но marker на плане не сигнализирует о нём.

8. Граница между отображением и поведением устройства

static_icon меняет только лицо marker. Следующие возможности продолжают работать без изменений:

  • клик, more-info, toggle, cover action, run action и подтверждение действия;
  • controls и управление связанными источниками света;
  • is_light и автоматическое определение светового источника;
  • Glow и заливка «Свет по источникам»;
  • световой вклад устройства в карточку комнаты и controls;
  • принадлежность пространству/комнате;
  • температура устройства как источник комнатной температуры, если это настроено;
  • данные в tooltip/more-info и предпросмотре настроек;
  • автоматическое обновление HA state внутри модели данных.

Если статичный marker выглядит доступным, но его active binding фактически имеет state unavailable, действие по нажатию проходит по существующему безопасному пути. Ошибка service call обязательно показывается существующим toast/HA feedback и не должна превращаться в молчаливый no-op. Новый режим не симулирует доступность сервиса и не подавляет ошибки действий.

Следствие: источник света в режиме static_icon может освещать комнату и создавать Glow, оставаясь сам визуально чёрным. Если пользователь не хочет, чтобы устройство влияло на освещение, для этого используется существующая настройка «Это устройство — источник света» и/или список управляемых источников, а не режим отображения.

9. Приоритеты и исключения видимости

Режим static_icon действует только на marker, который по общим правилам разрешено показывать.

Ситуация Приоритет над static_icon
Marker удалён с плана Не отображается и снова доступен к добавлению
Пользователь нажал «Скрыть» Не отображается в просмотре
В редакторе включено «Показать скрытые» Показывается как служебный blue ghost, а не как обычный чёрный marker
Устройство деактивировано в HA Принудительно скрыто в просмотре; в разрешённом design-preview показывается HA-disabled ghost
Привязка orphaned Обрабатывается по отдельному lifecycle-контракту disabled-devices; static_icon не возвращает marker на поверхность, где общий контракт запрещает его рендер, и не маскирует диагностику потерянной привязки
Marker выбран в редакторе Допускается внешняя рамка выделения
Keyboard focus Обязателен focus-visible outline для доступности
Новый marker Допускается служебная точка «новое устройство»

Служебные editor/diagnostic overlays не считаются реакцией на состояние устройства. Они не должны перекрашивать внутреннюю подложку обычного видимого статичного marker.

10. Hover, focus и действия

Интерактивные состояния интерфейса не входят в понятие статичности устройства и сохраняют текущее общее поведение marker:

  • hover продолжает использовать существующую accent-подсветку;
  • pointer cursor, tooltip и действие по клику сохраняются;
  • keyboard focus обозначается существующим focus-состоянием;
  • выбор marker в редакторе обозначается существующей рамкой/тенью;
  • drag feedback сохраняется, но не должен имитировать HA-состояния on, open, alarm или unavailable.

После завершения hover/focus/selection/drag marker возвращается к своей нейтральной статичной подложке. Новый режим подавляет только визуальные реакции на данные и состояния устройства, но не обратную связь на действия пользователя.

11. Предпросмотр устройства

hp-device-preview обязан использовать тот же ResolvedDevicePresentation, что и план и houseplan-space-card.

При выборе static_icon:

  • stage сразу показывает нейтральную подложку и базовую иконку;
  • кнопка демонстрации activity не показывается;
  • строка «Текущее состояние» продолжает показывать фактический state HA;
  • строка «Источник отображения» продолжает показывать сущность/сущности;
  • строка интеграции продолжает показывать provider;
  • строка «Результат» показывает локализованное объяснение: RU: «Статичный режим: состояние устройства не меняет значок»; EN: «Static mode: device state does not change the icon»;
  • тревога, недоступность и активность могут быть видны в фактах/технических подробностях, но не меняют stage;
  • режим не создаёт и не изменяет activity runtime.

Если привязка содержит alarm-capable сущность (smoke, gas, carbon_monoxide, moisture, safety, tamper, problem, siren или alarm panel), под select показывается неблокирующее предупреждение: статичный режим скроет визуальную тревогу marker. Предупреждение не запрещает Save и не зависит от того, активна ли тревога прямо сейчас.

В PresentationReason добавляется отдельная причина static_icon. Она имеет приоритет после ha_disabled/orphaned, но до alarm/availability/state причин для обычного renderable marker.

12. Автоматическая и ручная иконка

Статичный режим использует базовую effective icon:

  • ручной marker.icon, если он задан;
  • иначе иконку, определённую текущими правилами автоматического выбора.

State morph запрещён: например, cover не меняет mdi:...-closed на mdi:...-open при изменении state.

Автоматическая иконка не превращается в ручной override только из-за выбора static_icon. Поэтому она может измениться после реального изменения метаданных устройства или правил иконок. Если пользователю нужна полностью зафиксированная MDI-иконка и при изменении конфигурации HA, он должен нажать существующее действие фиксации автоматической иконки.

13. Модель данных и backend

13.1. Frontend

Канонический тип:

type DeviceDisplayMode = 'badge' | 'icon_ripple' | 'value' | 'static_icon';

ripple остаётся только legacy input и нормализуется в icon_ripple.

DISPLAY_MODES:

export const DISPLAY_MODES = ['badge', 'icon_ripple', 'value', 'static_icon'] as const;

Обязательно использовать единый экспортируемый DeviceDisplayMode и normalizer вместо повторения union и локальных _displayOf() в нескольких файлах. Неизвестное runtime-значение нормализуется в badge; ripple нормализуется в icon_ripple.

13.2. Persisted marker

display: static_icon

Поле display не переименовывается. Версия общей модели данных не меняется: это расширение уже существующего enum.

13.3. Backend

MARKER_SCHEMA принимает:

badge | ripple | icon_ripple | value | static_icon | null

Cross-language test обязан подтверждать, что каждое значение из frontend DISPLAY_MODES принимается backend-схемой.

14. Архитектурный план реализации

14.1. Единая точка поведения

Основная логика реализуется в resolveDevicePresentation(), а не отдельно в каждом renderer.

Предпросмотр продолжает вычислять sources и фактические HA states для объяснения результата. Плановым поверхностям разрешён явный short-circuit source/value resolution для static_icon, поскольку этим данным нечего менять в renderer-ready лице. Short-circuit не должен затронуть отдельные агрегаты света/климата/controls, которые вызывают свои resolver независимо от marker presentation.

14.2. Поверхности-потребители

Должны быть проверены:

  1. интерактивный план в houseplan-card;
  2. houseplan-space-card через space-render;
  3. hp-device-preview;
  4. режим устройства в editor и design preview;
  5. kiosk/view mode;
  6. скрытые и HA-disabled ghosts.

Ни одна поверхность не должна самостоятельно повторно классифицировать состояние или добавлять live badges после resolver.

14.3. CSS

Renderer должен иметь стабильный hook для статичного режима, например класс static-icon или data-display="static_icon", чтобы:

  • отключить перекраску внутренней подложки от HA state, не изменяя общий hover;
  • сохранить существующие hover, focus и editor selection;
  • не зависеть от порядка классов on/open/alarm/unavail;
  • дать одинаковый результат в основном плане, предпросмотре и статической карточке.

Не допускается решать задачу только более специфичным CSS поверх динамических классов: semantic projection тоже обязана быть нейтральной, чтобы tooltip, preview explanation и будущие renderers не получали ложное состояние.

14.4. Activity runtime

  • static_icon никогда не отображает уже активный event window;
  • preview demo останавливается при переключении на static_icon;
  • накопленный runtime не должен менять статичный marker;
  • после перехода обратно в icon_ripple не следует искусственно запускать activity только из-за смены режима;
  • реальные последующие события после возврата в icon_ripple работают обычно.

15. Локализация и текстовая подсказка

Обновить RU/EN ключи:

display.badge
display.static_icon
marker.display_hint
marker.preview.reason.static_icon

Рекомендуемая RU-подсказка:

Динамическая подложка показывает работу, открытие, тревоги и недоступность. «Значок + активность» дополнительно показывает события и движение. «Значение вместо иконки» выводит однозначное состояние HA. Статичный значок не меняется от состояний устройства.

Рекомендуемая EN-подсказка:

The dynamic plate shows work, open, alarm and unavailable states. Icon + activity also shows events and motion. Value instead of an icon shows one unambiguous HA state. A static icon does not react to device states.

Длинный ярлык display.badge проверяется в узком native select. Обрезка закрытого системного select допустима только если после открытия ОС показывает полный текст option; горизонтальный scroll или нарушение ширины диалога недопустимы.

16. Accessibility

  • Названия режимов должны полностью озвучиваться native select.
  • Контраст нейтральной иконки и подложки соответствует текущему marker contract.
  • static_icon не отменяет focus-visible.
  • Нельзя передавать состояние только через скрытую подложку: фактический state остаётся доступен в tooltip/more-info и в текстовом предпросмотре.
  • Для тревожных устройств пользователь осознанно отказывается от визуальной тревоги на marker; это должно быть понятно из предпросмотра и подсказки режима.
  • prefers-reduced-motion не требует отдельной ветки: в статичном режиме движения нет изначально.

17. Edge cases

17.1. Источник света

Свет включён, marker имеет display: static_icon и is_light: true: marker остаётся нейтральным, но Glow и заливка комнаты продолжают работать.

17.2. Critical alarm среди вторичных сущностей

Resolver продолжает находить critical source для диагностики, но лицо marker не становится красным. В предпросмотре current state/details тревога остаётся видна.

17.3. Нет HA state при загрузке

Marker сразу отображается нейтральным, без промежуточного unavail fade и без последующего визуального скачка после прихода states.

17.4. Виртуальное устройство

Виртуальный marker использует тот же статичный контракт. Его обычная dashed-рамка как признак типа объекта сохраняется, поскольку это не HA state.

17.5. Value → static

Текстовое значение немедленно заменяется базовой иконкой. Fallback notice value-режима исчезает.

17.6. Activity → static

Текущее кольцо исчезает немедленно. Возврат в activity-режим не создаёт новое событие сам по себе.

17.7. Автоматическая state icon

Открытие cover, lock или door не меняет glyph. При ручной смене icon в форме предпросмотр меняется — это конфигурационное действие пользователя, а не реакция на HA state.

17.8. Размер и поворот

Ручные size/angle применяются как обычно. «Статичный» не означает «не редактируемый».

17.9. Custom CSS/card-mod

Встроенный контракт гарантирует нейтральный вид без пользовательских overrides. Намеренный custom CSS может изменить marker; задача не должна пытаться перебить все внешние !important-правила.

18. Тестовый план для этапа реализации

По действующему правилу проекта тесты запускаются перед пре-релизом; ниже перечислен обязательный набор тестов, который должен быть добавлен вместе с реализацией.

18.1. Unit: presentation resolver

Для static_icon проверить минимум:

  1. on и off дают одинаковую visual projection;
  2. unknown и unavailable не дают unavail;
  3. alarm не даёт alarm;
  4. presence/event/transition/running не дают activity;
  5. cover state не меняет базовую иконку;
  6. RGB light не задаёт lightColor/динамическую окраску;
  7. value/temp/hum/LQI отсутствуют;
  8. size и angle сохраняются;
  9. source facts/signature остаются доступными;
  10. hidden/HA-disabled precedence соблюдается;
  11. explanation reason равен static_icon для обычного marker;
  12. переход между display modes не создаёт activity.
  13. live vacuum projection помечается как подавленная, а plan renderer не рисует puck/след в static_icon.

18.2. Backend

  • static_icon принимается MARKER_SCHEMA;
  • неизвестное значение отклоняется;
  • cross-language enum test проходит;
  • legacy ripple и null по-прежнему принимаются.

18.3. UI/component

  • в select четыре пункта в заданном порядке;
  • badge выбран по умолчанию;
  • новое название badge локализовано RU/EN;
  • static_icon последним в RU/EN;
  • activity settings скрыты для static_icon;
  • preview сразу становится нейтральным;
  • activity demo отсутствует;
  • hover работает так же, как у остальных marker;
  • focus-visible и editor selection остаются видимыми;
  • сохранение/повторное открытие сохраняет static_icon.
  • alarm-capable binding получает неблокирующее предупреждение;
  • tap по unavailable binding в static_icon показывает видимую ошибку service call, а не молчит;
  • длинный RU/EN ярлык не создаёт горизонтальный scroll на узком экране.

18.4. Поверхностный parity test

Один и тот же draft/config marker при одинаковых входных данных имеет одинаковые icon/classes/badges в:

  • основном плане;
  • hp-device-preview;
  • houseplan-space-card.

Parity-набор включает отдельную vacuum-фикстуру: base marker статичен, а puck/след отсутствуют. Минимальный browser smoke проверяет alarm-устройство в static_icon (нет alarm, unavail, activity-*), мгновенное переключение режима туда и обратно, preview parity и отсутствие vacuum overlay.

19. Документация реализации

В той же локальной серии изменений обновляются:

  • docs/CHANGELOG.md и docs/CHANGELOG.ru.md отдельной user-visible записью;
  • docs/USER-GUIDE.ru.md — четыре режима и их матрица;
  • docs/ARCHITECTURE.md и docs/STATUS.md — enum и единая static projection;
  • docs/FILTERING.md — исключение из общей state-матрицы;
  • docs/VACUUM.md — подавление puck/следа;
  • docs/TESTING.md и docs/TESTING-DEMO.md — unit/backend/browser/manual сценарии;
  • существующие скриншоты редактора с прежним списком режимов, если такие актуальные скриншоты обнаружены.

20. Критерии приёмки

  1. В списке «Отображение» четыре режима, static_icon расположен последним.
  2. Текущий «Значок» называется «Значок + динамическая подложка».
  3. Новые и старые устройства остаются в динамическом badge по умолчанию.
  4. Устройство в static_icon выглядит одинаково при on, off, working, open, alarm, unknown и unavailable.
  5. Статичный marker не меняет иконку по state, не получает activity, RGB, value, temperature, humidity или LQI.
  6. Размер, угол и базовая иконка применяются.
  7. Световое и управляющее поведение устройства не меняется.
  8. Скрытые, удалённые и HA-disabled устройства не возвращаются на план из-за static_icon.
  9. Hover, focus и selection работают так же, как у остальных marker, и не изменяются этой задачей.
  10. Предпросмотр точно объясняет статичный результат, сохраняя фактический HA state в текстовых фактах.
  11. Backend принимает новый enum, существующие конфигурации не мигрируют и не ломаются.
  12. Все три renderer-поверхности используют единый resolver и показывают одинаковый результат.
  13. Живой пылесос в static_icon не показывает puck, след или room-highlight, но сохранённая история не удаляется.
  14. Ошибка действия по недоступной сущности остаётся видимой пользователю.
  15. Alarm-capable binding получает честное неблокирующее предупреждение до Save.

21. Подтверждённые продуктовые решения

Владелец продукта подтвердил 2026-08-08:

  1. Статичность касается только внешнего вида marker. Устройство продолжает влиять на Glow, заливку «Свет» и controls согласно своим отдельным настройкам и фактическому состоянию.
  2. У статичного marker полностью скрываются живые satellite badges температуры, влажности и LQI.
  3. Hover не изменяется этой задачей и сохраняет текущее общее поведение marker, включая существующую accent-подсветку. Статичность относится к HA state и данным устройства, а не к интерактивной обратной связи UI.

Тревоги, unknown/unavailable, state icon morph и RGB-цвет также безусловно подавляются, поскольку статичный marker должен иметь одинаковый вид при любых состояниях устройства.