# HP-UX-03 — предпросмотр отображения устройства Статус: **реализовано и проверено для v1.60.2-beta.3** Дата: **2026-08-08** Область: **диалог добавления/редактирования устройства, единый resolver визуального состояния, текстовый режим значения и происхождение привязки из Home Assistant** ## 1. Зафиксированное продуктовое решение В диалоге устройства появляется живой блок **«Предпросмотр»**, который до сохранения показывает ровно тот marker, который пользователь получит на плане с текущими значениями формы. Предпросмотр обязательно отображает: 1. фактический визуал marker прямо сейчас; 2. интеграцию Home Assistant, которой принадлежит выбранная привязка; 3. сущность или набор сущностей, из которых сейчас получен визуал; 4. локализованное текущее состояние; 5. понятное объяснение результата: нейтральная/жёлтая/оранжевая/красная/приглушённая подложка, наличие и тип активности, значение либо fallback к иконке; 6. несохранённые изменения иконки, режима отображения, размера, поворота, controls, роли источника света и tap action; 7. отдельную безопасную демонстрацию обычного activity-эффекта, когда реальное устройство сейчас не активно. Предпросмотр не является второй независимой реализацией визуала. Interactive план, диалог и статическая `houseplan-space-card` используют один `ResolvedDevicePresentation` и один renderer лица marker. ## 2. Проблема текущего интерфейса Поле «Отображение» предлагает три варианта: - «Значок»; - «Значок + активность»; - «Значение вместо иконки». Пользователь до сохранения не видит: - какую подложку получит конкретное устройство; - почему работающий объект стал или не стал жёлтым; - какая сущность выбрана источником состояния среди нескольких сущностей устройства; - что именно добавляет «Значок + активность», если сейчас нет движения/события; - какое значение появится вместо иконки и почему иногда остаётся иконка; - как пользовательский размер, угол, icon и activity color выглядят вместе; - не взяла ли логика вспомогательный switch, status LED или диагностическую сущность; - какой интеграцией HA фактически предоставлена привязка. Сейчас визуал строится поздно, уже на плане, через несколько связанных функций (`_visualSamples`, `_deviceVisual`, `_stateClass`, `_actEntity`, форматирование value и `_renderDevice`). Попытка нарисовать упрощённый пример прямо в форме неминуемо создаст расхождение с настоящим marker. ## 3. Цели 1. Дать точный ответ «как это устройство будет выглядеть на плане сейчас» до Save. 2. Объяснить, почему получен именно такой визуал, не требуя знания entity ID и внутренних приоритетов. 3. Сделать режимы «Значок» и «Значок + активность» различимыми даже у спокойного устройства. 4. Сделать «Значение вместо иконки» предсказуемым для числовых и текстовых состояний. 5. Показывать реальную интеграцию-провайдера без эвристик по имени/модели. 6. Исключить повторную реализацию логики отображения внутри диалога. 7. Не менять данные плана, HA state или runtime активности до подтверждённого Save. 8. Подготовить самостоятельный компонент, который можно будет перенести при будущем разделении длинного диалога по HP-UX-04. ## 4. Не входит в задачу - симулятор всех возможных состояний сущности; - управление устройством из предпросмотра; - изменение state или service call в Home Assistant; - редактирование интеграции, config entry, entity registry или device registry; - настройка отдельных цветов для working/open/alarm; - новая пользовательская настройка источника состояния; - полный предпросмотр комнатного Glow, солнечных лучей или заливки комнаты; - отдельный каталог/логотипы интеграций; - сохранение названия интеграции в marker; - изменение модели `display` или добавление нового режима; - изменение визуальной семантики, уже утверждённой в `2026-08-05-device-visual-state-design.md`, кроме явно описанного ниже расширения текстового value. ## 5. Термины - **Binding provider** — интеграция HA, которой принадлежит выбранная `device:*` или `entity:*` привязка. - **Visual source** — сущность или набор сущностей, по которым resolver вычисляет подложку и activity marker прямо сейчас. - **Value source** — единственная сущность/derived reading, текст которой может заменить иконку. - **Actual preview** — реальный результат для текущих HA states и несохранённых полей формы. - **Effect demo** — локальная краткая демонстрация ordinary activity без изменения HA и без записи runtime-события marker. - **Draft marker** — временная проекция полей открытого диалога, не входящая в `_markers` и server config. - **Fallback to icon** — предсказуемое отображение базовой/морфирующейся иконки, когда value получить нельзя. ## 6. Размещение и структура интерфейса ### 6.1. Место в диалоге Блок располагается непосредственно после select **«Отображение»** и его короткой подсказки. Настройки activity color/size и общий size/rotation остаются рядом с ним. Порядок секции: 1. Иконка. 2. Отображение. 3. Предпросмотр. 4. Настройки activity, если выбран `icon_ripple`. 5. Размер и поворот. Предпросмотр обновляется и от последующих полей, поэтому после изменения activity/size/angle пользователь остаётся в той же прокрученной области и сразу видит результат выше. Допускается сделать stage `position: sticky` только внутри широкого desktop layout при доказанном отсутствии конфликтов с `hp-dialog`; базовое ТЗ требует обычный блок в потоке без вложенной прокрутки. ### 6.2. Состав блока ```text Предпросмотр [Сейчас] ┌──────────────────── stage ─────────────────────┐ │ [ marker ] │ └────────────────────────────────────────────────┘ Предоставлено интеграцией Local Tuya (localtuya) Источник отображения Water switch · switch.water_switch Текущее состояние Включено Результат Работает · жёлтая подложка [Показать пример активности] — только для «Значок + активность» ``` Обязательные части: - заголовок «Предпросмотр»; - badge **«Сейчас»** или **«Пример»**; - визуальная stage; - всегда видимая строка интеграции; - краткие строки источника, состояния и результата; - раскрываемые технические подробности для сложных случаев; - demo-кнопка только когда она имеет смысл. ### 6.3. Визуальная stage - нейтральный фон использует текущие theme tokens House Plan, а не белый hardcode; - stage не изображает комнату и не обещает точный Glow; - marker использует те же CSS classes, CSS variables, icon morph, value, badges и activity ring, что на плане; - stage не имеет pointer actions marker и не открывает more-info; - max `size`/`ripple_size` не создаёт horizontal scroll и не обрезается молча; - если полный bounding diameter не помещается, весь preview-face пропорционально уменьшается, а рядом появляется подпись `Предпросмотр уменьшен до 60%`; отношение icon/ring сохраняется; - фактический размер на плане продолжает определяться card `icon_size`, marker `size` и текущим zoom; stage показывает пропорции, а не сантиметры. ## 7. Actual preview — основной контракт Actual preview является значением по умолчанию и отвечает только на вопрос «что видно сейчас». Он учитывает: - текущий `hass.states`; - актуальные device/entity registries; - выбранный draft binding; - draft `icon` или фактический auto icon; - draft `display`; - draft `tapAction`; - draft `controls`; - draft `isLight`; - draft `useClimateTemp`; - draft `rippleColor`, `rippleSize`, `size`, `angle`; - card `live_states`, `show_temperature`, `show_signal`; - выбранную/автоматически определённую комнату и effective space settings, необходимые для тех же badges, что на плане; - существующий activity runtime исходного marker, если редактируется уже размещённый объект и binding/source не изменился. Для этого пункта «source не изменился» имеет формальное значение: совпадает `activitySourceSignature`, построенная из binding, effective `sourceKind`, отсортированных пар `role + entity_id` всех effective visual/critical sources и текущего value source. Порядок ключей в registry и порядок одинаковых controls не влияют на сигнатуру; добавление, удаление или замена effective control, изменение роли сущности либо binding меняют её. Runtime window исходного marker можно читать только при точном совпадении этой сигнатуры. Actual preview не учитывает как визуальные настройки marker: - имя; - model/description/link/manuals; - несохранённую room position; - координаты marker; - hide/show как ghost-стиль. Если marker пользовательски скрыт, preview всё равно показывает его будущий **видимый** дизайн, а рядом выводит состояние `Скрыто с плана`. Иначе ghost подавит значения/эффекты и предпросмотр перестанет решать задачу. ## 8. Effect demo ### 8.1. Причина У спокойного устройства «Значок» и «Значок + активность» выглядят одинаково. Пользователь должен увидеть выбранные цвет и размер кольца до реального движения или события. ### 8.2. Поведение - Кнопка RU: **«Показать пример активности»**. - EN: **“Preview activity effect”**. - Доступна только при `display: icon_ripple`, когда сейчас нет ни alarm, ни реального ordinary activity (`event`, `presence`, `transition`, `running`). - Во время реального ordinary activity кнопка disabled и имеет подсказку `Эффект уже виден в реальном состоянии`. - Клик запускает локальный `event`-эффект на 3,3 секунды с draft color/size. - Badge `Сейчас` временно меняется на `Пример`. - Строки интеграции, source и current state продолжают показывать реальные данные. - Result явно говорит `Демонстрация эффекта; состояние HA не изменено`. - Повторный клик перезапускает окно. - Закрытие диалога немедленно очищает demo timer. - Demo не пишет `_activityRt`, не создаёт event marker, не вызывает service и не влияет на marker на плане. - Любой новый реальный эффект (`event`, `presence`, `transition`, `running` или `alarm`) немедленно прекращает demo. Реальность всегда имеет приоритет; draft-кольцо и фактическое кольцо одновременно не рисуются. - `prefers-reduced-motion` показывает статическое кольцо и объясняет, что анимация отключена системной настройкой. Постоянный симулятор working/open/alarm не добавляется: он создаст второй state editor и смешает реальность с примером. ## 9. Интеграция, предоставившая устройство ### 9.1. Две разные строки Предпросмотр различает: 1. **«Предоставлено интеграцией»** — владелец binding; 2. **«Источник отображения»** — сущность, которая фактически определяет визуал. Пример: ```text Предоставлено интеграцией Local Tuya (localtuya) Источник отображения Лампа кухни · light.kitchen · Hue ``` Это корректно для marker, привязанного к Local Tuya switch, но управляющего Hue light через `controls`. ### 9.2. Авторитетный resolver провайдера ```ts interface IntegrationProvider { domain: string; label: string; configEntryId?: string; configEntryTitle?: string; confidence: 'registry-owner' | 'entity-platform' | 'identifier-fallback'; } resolveBindingProviders(hass, binding, metadata): IntegrationProvider[] ``` #### `device:` Приоритет: 1. `device.config_entry_id` в актуальном HA; 2. legacy `device.config_entries[]` до завершения совместимости HA; 3. уникальные `entity.platform` активных сущностей устройства; 4. domain из `device.identifiers` только как последний fallback. В Home Assistant 2026.8 устройство принадлежит одному config entry. Старый merged-device может временно дать несколько providers; UI показывает уникальный список, а не выбирает первый случайный. #### `entity:` Приоритет: 1. `entity.platform` как domain интеграции, создавшей сущность; 2. `entity.config_entry_id`, если доступен, для config entry title; 3. raw domain entity ID не является integration domain и не используется вместо `platform`. #### `virtual` ```text Предоставлено интеграцией House Plan · виртуальное устройство ``` Это явно не HA integration и не должно выглядеть как неизвестная ошибка. ### 9.3. Человекочитаемое название Для `domain` используется следующий fallback chain: 1. локализованное название компонента, уже доступное в HA frontend; 2. имя integration manifest; 3. config entry title, если он не дублирует пользовательское имя устройства; 4. raw domain в читаемом виде. Рекомендуемый вывод: `Local Tuya (localtuya)`, `MQTT (mqtt)`, `Template (template)`. Config entry title может выводиться второй строкой/tooltip, например `Котельная · localtuya`, но не подменяет название интеграции: title часто является пользовательским экземпляром, а не типом provider. ### 9.4. Запрещённые эвристики Нельзя определять интеграцию по: - имени marker; - `friendly_name`; - model/manufacturer; - ключевым словам `Tuya`, `Zigbee`, `Matter`; - первой сущности в несортированном объекте; - только `identifiers[0][0]`, когда registry/config entry metadata доступны. Существующий `domainOfDevice()` решает внутреннюю задачу фильтрации и не должен автоматически становиться пользовательским resolver интеграции. ### 9.5. Несколько providers Для legacy merged device или нескольких источников: - строка binding provider показывает максимум два label и `ещё N`; - tooltip/раскрытые подробности содержат полный уникальный список; - сортировка стабильна: registry-owner первым, затем label/domain; - повторяющиеся domains/config entries дедуплицируются; - providers external `controls` не смешиваются с владельцем binding, а показываются у соответствующих visual sources. ### 9.6. Загрузка metadata - получение config entries/manifests выполняется лениво только при открытом marker dialog; - результат кешируется на уровне card по registry/config revision; - открытие/Save не блокируются ожиданием красивого label; - raw `entity.platform` показывается сразу; - после загрузки manifest/config entry строка обновляется без скачка stage; - ошибка metadata lookup не ломает preview и не создаёт toast: остаётся raw domain; - внешние сетевые запросы к сайтам интеграций запрещены. Metadata-fetches допустимы в текущей архитектуре, потому что marker/device dialog доступен только при `_canEdit`, то есть администратору HA. Это явный инвариант функции, а не предположение о правах обычного пользователя. Если в будущем gate редактора будет ослаблен, доступность config-entry/manifest WS для не-администратора необходимо заново проверить, а недоступность metadata должна по-прежнему давать безопасный raw-domain fallback без поломки preview. ## 10. Источник отображения и объяснение ### 10.1. Один структурированный результат Текущие `_visualSamples`, `_deviceVisual`, `_stateClass`, `_actEntity`, value/icon/badge calculations преобразуются в общий pure projection: ```ts interface ResolvedDevicePresentation { binding: string; providers: IntegrationProvider[]; sourceKind: 'cover' | 'light' | 'controls' | 'device_role' | 'primary' | 'none'; visualSources: ResolvedPresentationSource[]; criticalSources: ResolvedPresentationSource[]; valueSource: ResolvedValueSource | null; visual: DeviceVisualState; display: 'badge' | 'icon_ripple' | 'value'; icon: string; valueText: string | null; valueFullText: string | null; fallbackReason: ValueFallbackReason | null; activity: DeviceActivity; classes: string[]; tempText: string | null; humText: string | null; lqiText: string | null; lightColor: string | null; explanation: PresentationExplanation; } ``` План и preview получают один объект; диалог лишь добавляет human-readable строки из `explanation`. ### 10.2. Приоритет источников Сохраняется текущий контракт: 1. cover, если draft tap action явно `cover`; 2. effective `resolvedLightSources`, включая external controls и явный `isLight`; 3. `resolvedDeviceStateEntities` по функциональной роли; 4. primary fallback; 5. критические сущности добавляются отдельно и могут переопределить результат alarm. Preview не получает упрощённый «первый entity» path. ### 10.3. Пользовательское объяснение Технические enums не показываются как основной текст. | Internal | RU result | |---|---| | `available + neutral + none` | `Доступно · нейтральный значок` | | `working + running`, badge | `Работает · жёлтая подложка` | | `working + running`, icon_ripple | `Работает · жёлтая подложка и активность` | | `open` | `Открыто/разблокировано · оранжевая подложка` | | cover open | `Открыто · состояние показано формой иконки` | | `presence` | `Присутствие обнаружено · постоянное кольцо` | | `event` | `Зафиксировано событие · короткие волны` | | `transition` | `Устройство движется · кольцо до завершения` | | `unavailable` | `Недоступно · приглушённое отображение` | | `alarm` | `Тревога · красная подложка и критическая пульсация` | Для media player preview явно объясняет нейтральное powered/playing состояние: `Воспроизведение не считается работой устройства`. Для cover объясняет отсутствие жёлтой подложки. Для composite switch с Power entity объясняет: `Состояние берётся из питания; вспомогательные переключатели не учитываются`. ### 10.4. Несколько visual sources Основная строка показывает: - один источник — friendly name + entity ID; - два — оба friendly names; - три и более — `3 источника` с кратким агрегированным состоянием. Раскрытие «Подробнее» показывает таблицу: | Поле | Содержание | |---|---| | Сущность | локализованное имя + `entity_id` | | Интеграция | provider этой сущности | | Состояние | `hass.formatEntityState` | | Роль | основной источник / control / свет / критическая тревога | Debug IDs доступны, но не загромождают основной UX. ### 10.5. Особый случай пылесоса Для marker с `marker.vacuum.live` preview показывает только общее **лицо базы**: иконку или value, подложку, badges и activity по тем же правилам, что обычный marker. Отдельный движущийся puck робота и клиентский/серверный след внутри preview-stage не рисуются: это координатная механика плана, а не часть face. Explanation дополнительно сообщает: `Живой робот и след отображаются на плане`. На самом плане puck и trail продолжают использовать существующий runtime; общий presentation resolver определяет лицо базы и не получает ответственность за геометрию или историю следа. ## 11. Поведение трёх display modes ### 11.1. «Значок» (`badge`) - показывает effective icon; - учитывает state icon morph; - показывает status plate; - не показывает ordinary activity; - alarm всё равно показывает красную critical activity; - temp/hum/LQI badges следуют фактическим настройкам карточки; - Result объясняет, что обычная активность отключена выбранным display. ### 11.2. «Значок + активность» (`icon_ripple`) - включает всё из «Значок»; - добавляет semantic event/presence/transition/running activity; - использует draft `rippleColor` и `rippleSize` для ordinary activity; - alarm всегда использует критический красный стиль, игнорируя пользовательский цвет; - при отсутствии реальной активности stage честно совпадает с «Значок», а demo-кнопка показывает разницу; - если `live_states: false`, actual preview нейтрален, explanation сообщает `Живые состояния выключены в настройках карточки`; demo остаётся доступной как помеченный пример. ### 11.3. «Значение вместо иконки» (`value`) Режим расширяется до числовых **и текстовых** состояний без нового config value. Приоритет value: 1. derived temperature, когда текущий marker действительно показывает climate/device temperature; 2. derived humidity по текущей логике marker; 3. одна однозначная `valueSource` из effective source resolver; 4. иначе fallback к icon. Форматирование: - использовать `hass.formatEntityState` / существующий `hassValue`; - numeric obeys `display_precision`, locale and unit HA; - text state локализуется HA (`on` → `Включено`, `open` → `Открыто`); - unit не дублируется; - текст в одну строку, с max width и ellipsis; - полное значение доступно в `title`, `aria-label` и explanation; - собственное округление House Plan запрещено. Одинаковый контракт длинного value применяется на основном плане, в preview и в `houseplan-space-card`: - face сохраняет существующий `min-width: var(--dev-size)` и внутренние отступы; - ширина может расти с текстом, но ограничивается `max-width: calc(var(--dev-size) * 4)`; - `.valtext` занимает доступную ширину и использует `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`; - hit area и layout-box marker ограничены тем же max-width и не расширяются до полного скрытого текста; - полный локализованный текст доступен в `title`, `aria-label` и explanation; - renderer не переносит value на вторую строку и не влияет полным текстом на соседние hit areas, автокомпоновку или kiosk layout. Fallback к icon применяется, когда: - нет state object; - state пуст; - state `unknown`/`unavailable`; - visual source set неоднозначен и нельзя честно выбрать одно value; - значение не является scalar string/number/boolean; - binding virtual и не имеет state. Слишком длинная, но валидная строка **не** вызывает fallback: она сокращается визуально через ellipsis, полное значение сохраняется доступным. Preview обязан написать причину настоящего fallback, например `Несколько источников состояния — показана иконка`. Статусная подложка и alarm продолжают применяться к value badge. Обычная activity в value mode не рисуется. ### 11.4. Однозначность value source Value source считается однозначным, если effective visual role содержит одну сущность. Для нескольких controls/lights/passive sources нельзя молча брать первый элемент объекта или primary другой роли. Это намеренное уточнение текущего поведения. Оно предотвращает ситуацию, когда подложка агрегируется по группе ламп, а текст неожиданно берётся из несвязанного primary устройства. ## 12. Draft updates | Изменение формы | Обновление preview | |---|---| | Binding | новый DevItem, provider, source, state, auto icon и visual | | Virtual/HA mode | provider House Plan либо HA; state reset | | Controls add/remove | пересчитать sources, status, activity, value ambiguity и source integrations | | Tap action → cover | cover становится source/value/icon morph до Save | | `isLight` | пересчитать light source и working status | | `useClimateTemp` | добавить/убрать derived temperature value/badge | | Custom icon | мгновенно заменить base icon | | Reset to auto icon | восстановить актуальный auto icon без записи override | | Display | немедленно сменить icon/activity/value presentation | | Activity color/size | обновить реальную activity или следующий demo frame | | Size/angle | обновить face и fit scale | | Room | пересчитать effective space display options/badges | | Name/model/link/description/files | визуал не меняется; provider не пересчитывается по этим полям | | Hide/Show draft | только badge состояния формы; face остаётся design preview | Все обновления происходят без Save и без мутации `_serverCfg`, `_markers`, `_devices`, layout и runtime state исходного marker. ## 13. Реактивность HA Пока диалог открыт: - новый `hass.states` пересчитывает preview; - реальное working/presence/transition появляется и исчезает live; - короткий witnessed event существующего marker использует его текущее runtime window; - после смены binding/source старое event window не переносится; - новый draft binding получает baseline и не генерирует ложное событие от первого snapshot; - device/entity registry update обновляет names, provider, source и disabled/orphaned status; - завершение 3,3-секундного окна вызывает repaint preview; - закрытый/backgrounded dialog не держит timers. Если во время Effect demo приходит любое реальное ordinary/critical событие, demo отменяется до вычисления следующего кадра и preview сразу показывает фактический presentation. Если dialog открыт в момент внешнего изменения server config, применяется существующая optimistic/revision policy. Preview сам не является основанием перезаписывать внешний config. ## 14. Состояния binding | Binding | Preview | |---|---| | Active | полный actual preview | | Active, state unavailable | приглушённый marker, integration/source видны | | HA-disabled | служебный disabled face без live status; explanation с причиной; Show остаётся заблокирован по отдельному ТЗ | | Orphaned | `mdi:help-circle`/сохранённый icon, integration `не определена`, source `привязка не найдена` | | Removed tombstone в Add | не выбирается; preview не создаётся | | User-hidden | видимый design preview + badge `Скрыто с плана` | | Virtual | neutral custom/auto icon; provider House Plan; value fallback reason | | Binding ещё не выбран | placeholder и инструкция выбрать устройство | Preview использует центральный resolver из `2026-08-08-ha-disabled-devices-design.md`. Локальные проверки `disabled_by` и отдельная имитация disabled/orphaned внутри preview запрещены. ## 15. Responsive layout и touch ### 15.1. Desktop Reference environment: desktop browser, fine pointer, mouse/keyboard. - при достаточной ширине stage и explanation располагаются двумя колонками; - minimum text column позволяет длинным RU/EN labels переноситься; - entity IDs используют `overflow-wrap: anywhere`; - footer `hp-dialog` не сдвигается и не обрезается; - блок не создаёт отдельного вертикального/горизонтального scrollbar; - stage не меняет ширину диалога при activity ring. ### 15.2. Touch classification По `docs/TOUCH-SUPPORT.md`: - **Device editor: best effort / intentionally degraded.** Полный комфорт preview гарантируется на desktop. - На touch preview, если показан, складывается в одну колонку и не должен перекрывать Save/Cancel/Delete/Hide. - Effect demo может отсутствовать на touch, если его корректный responsive вариант дорог. - Интеграция и текстовое explanation остаются доступны, если сам dialog устройства доступен. - Safety floor обязателен: preview не вызывает service, не меняет config и не ломает выход в View. - Дорогая touch-полировка не должна усложнять общий renderer или View. ### 15.3. Narrow desktop Небольшая ширина окна с fine pointer остаётся поддерживаемым desktop editing case. В отличие от touch-устройства, narrow desktop обязан сохранить всю функциональность preview в stacked layout без horizontal scroll. ## 16. Доступность - Stage имеет `role="img"` и полный локализованный `aria-label`: имя marker, integration, state, visual result. - Цвет не является единственным объяснением: Result всегда называет смысл текстом. - Техническое раскрытие доступно с клавиатуры. - Demo — настоящий `