Files
houseplan-card/docs/superpowers/specs/2026-08-08-ha-disabled-devices-design.md
T

66 KiB
Raw Blame History

Деактивированные устройства Home Assistant — техническое задание

Статус: реализовано и проверено для v1.60.2-beta.3 Дата: 2026-08-08 Область: устройства и сущности HA, уже добавленные на план, автоматическое обнаружение, редактор устройств и все производные данные плана

1. Краткое решение

Если привязанный к плану объект деактивирован в Home Assistant, House Plan должен:

  1. автоматически убрать его из обычного отображения плана;
  2. не учитывать его состояния и данные ни в одном расчёте или эффекте плана;
  3. сохранить саму привязку, позицию и все пользовательские настройки;
  4. показывать его только как отдельный служебный ghost в редакторе устройств при включённом показе скрытых объектов;
  5. при попытке нажать «Показать» сообщать, что деактивированное в Home Assistant устройство нельзя показать на плане;
  6. автоматически вернуть его после активации в HA, если до деактивации пользователь не скрывал его вручную;
  7. не возвращать его автоматически, если пользовательское marker.hidden: true уже было установлено;
  8. никогда не превращать временную деактивацию HA в постоянное пользовательское скрытие.

Состояние деактивации вычисляется в runtime по реестрам Home Assistant. Новое сохраняемое поле и миграция модели не нужны.

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

Сейчас House Plan строит устройства по hass.devices, hass.entities и hass.states, но не использует disabled_by как обязательный фильтр. Из-за этого:

  • деактивированное устройство может продолжать отображаться на плане;
  • его вспомогательная или оставшаяся в кеше сущность может ошибочно стать основной;
  • деактивированная сущность может влиять на цвет подложки, Glow, свет, климат, LQI, тревоги или действия;
  • в списках добавления могут одновременно появляться рабочий и деактивированный экземпляры одного физического устройства;
  • простая проверка наличия state не решает проблему: unknown, unavailable, задержка загрузки state и настоящая деактивация — разные состояния.

Home Assistant считает запись реестра сущности деактивированной, когда disabled_by не равен null; такая сущность не должна загружаться в state machine. Устройство также имеет собственный реестровый disabled_by. Поэтому источником истины должны быть полные реестры HA, а не текущее значение state.

3. Цели

  1. Системно исключить деактивированные устройства и сущности из визуала, управления и данных плана.
  2. Сохранить пользовательскую конфигурацию без необратимых автоматических изменений.
  3. Корректно обработать деактивацию и повторную активацию в любой момент жизненного цикла карточки.
  4. Не смешивать четыре разных понятия: пользовательское скрытие, деактивацию HA, недоступность и удаление с плана.
  5. Обеспечить одинаковое безопасное поведение во всех режимах, карточках и резолверах; при недостаточных правах на полный registry допускается явно описанная консервативная деградация без ложного disabled-статуса.
  6. Исключить локальные исключения по моделям, доменам или названиям устройств.

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

  • включение или выключение устройств/сущностей внутри Home Assistant из House Plan;
  • изменение реестров HA;
  • автоматическое удаление привязки к плану;
  • автоматический поиск нового device_id или entity_id, если интеграция пересоздала объект;
  • изменение существующей семантики off, unknown и unavailable;
  • исправление общего поведения потерянных/удалённых из реестра привязок — они описываются как orphaned, но не приравниваются к деактивации;
  • изменение формата Marker, серверной схемы или версии модели данных.

5. Термины

  • Пользовательски скрытое устройство — привязка с сохранённым marker.hidden: true.
  • Деактивированная привязка HA — привязка, для которой центральный runtime-резолвер вернул ha_disabled.
  • Эффективно скрытое устройство — пользовательски скрытое или деактивированное в HA устройство.
  • Удалённое с плана устройство — привязка с marker.removed: true; это tombstone, а не скрытый объект.
  • Недоступное устройство — активная в реестре привязка со state unavailable, unknown либо без полученного state.
  • Потерянная привязка — сохранённый ID отсутствует в полном реестре HA. Это не доказательство деактивации.
  • Активная сущность — реестровая сущность с disabled_by == null и без деактивации родительского устройства.
  • Сохранённая привязка — существующий marker с binding: device:* или binding: entity:*, не помеченный removed: true.
  • Неподтверждённая привязка — сохранённый HA-binding, для которого клиент без доступа к полному registry не видит ни достаточного registry-свидетельства активности, ни live-state. Это unverified, а не ha_disabled и не orphaned.

6. Базовые инварианты

  1. marker.hidden выражает только явное решение пользователя.
  2. House Plan не записывает marker.hidden: true при деактивации в HA.
  3. marker.removed: true имеет приоритет над всеми остальными состояниями: объект не строится даже как ghost.
  4. unknown, unavailable, отсутствие state и ошибка интеграции сами по себе не означают ha_disabled.
  5. hidden_by сущности HA не равен disabled_by: скрытая в интерфейсе HA сущность остаётся рабочей для House Plan.
  6. Любое ненулевое значение disabled_by считается деактивацией независимо от причины (user, integration, config_entry, device, system или новое значение будущей версии HA).
  7. Деактивированная сущность не может стать primary, источником состояния, света, климата, LQI, тревоги или действия.
  8. Виртуальные устройства House Plan не имеют состояния ha_disabled.
  9. Вся логика использует один резолвер статуса привязки; отдельные проверки в Glow, карточке комнаты, controls и рендере запрещены.
  10. Изменение disabled_by должно перестраивать список устройств, даже если число записей в реестрах не изменилось.
  11. Отказ WS registry-команды по правам не считается доказательством деактивации или удаления объекта.
  12. При unverified запрещены рендер обычного marker, использование данных и service actions; disabled-ghost и текст «деактивировано» также запрещены.

7. Авторитетный источник состояния

7.1. Данные

Для определения деактивации используются:

  • полный device registry HA: device_id, disabled_by;
  • полный entity registry HA: entity_id, device_id, disabled_by;
  • связь сущности с родительским устройством.

hass.states используется только после определения активного набора сущностей и никогда не является источником факта деактивации.

Если текущая версия frontend предоставляет в hass.entities только активное сокращённое представление, реализация должна получить или подписать полное представление entity registry. Отсутствие сущности в сокращённом списке нельзя автоматически трактовать как деактивацию.

До реализации проверяются права на config/entity_registry/list и config/device_registry/list в минимальной и максимальной поддерживаемых версиях HA под admin и read-only пользователем. Ошибка unauthorized, отсутствие команды или временная ошибка WS переводят источник registry в режим limited, не ломают карточку и не запускают повторный запрос на каждом state tick.

Если полный снимок уже авторитетен, но живая frontend-проекция получила строку, которой в снимке ещё нет, строка немедленно добавляется как положительное свидетельство. Смена identity живой device/entity-проекции также запускает debounced повторную загрузку полных реестров. Это страхует discovery и флипы disabled_by, когда registry event subscription недоступна или оборвалась.

Fallback в режиме limited:

  1. запись с доступным disabled_by != null остаётся авторитетным свидетельством ha_disabled;
  2. запись есть в сокращённом registry и не disabled — binding считается активным; отсутствие state означает обычный unknown/unavailable, а не деактивацию;
  3. записи нет в сокращённом registry, но точный entity_id имеет live-state — entity-binding считается активным;
  4. для device-binding live-state считается свидетельством активности только через доступную связь entity.device_id, без эвристики по имени;
  5. записи и live-state нет — результат unverified: обычный marker, данные и действия не рендерятся, но disabled-ghost/banner и обещание «активировать в HA» не показываются;
  6. последний авторитетно известный ha_disabled из локального runtime-кеша сохраняет forced-hidden до успешной перепроверки; кешированный active сам по себе не заменяет актуальное registry/state-свидетельство активности.

Таким образом read-only View остаётся безопасным. Полная UX-семантика disabled-ghost гарантируется там, где полный registry доступен; limited-клиент не выдаёт догадку за факт.

7.2. Результат центрального резолвера

type HaBindingStatus =
  | {
      kind: 'active';
      enabledEntityIds: string[];
      allEntityIds: string[];
    }
  | {
      kind: 'ha_disabled';
      reason: 'device' | 'entity' | 'all_entities';
      enabledEntityIds: [];
      allEntityIds: string[];
    }
  | {
      kind: 'orphaned';
      reason: 'device_missing' | 'entity_missing';
      enabledEntityIds: [];
      allEntityIds: string[];
    }
  | {
      kind: 'unverified';
      reason: 'registry_unavailable';
      enabledEntityIds: [];
      allEntityIds: string[];
    };

Конкретные имена типов могут измениться, но различие active / ha_disabled / orphaned / unverified обязательно.

7.3. Правила определения

Тип привязки Условие Результат
device:<id> устройство найдено и device.disabled_by != null ha_disabled / device
device:<id> устройство активно, есть дочерние сущности, и все они деактивированы ha_disabled / all_entities
device:<id> устройство активно, хотя бы одна дочерняя сущность активна active; в runtime попадают только активные сущности
device:<id> устройство активно и в полном реестре у него нет ни одной сущности не считать деактивированным; сохранить текущее поведение устройства без сущностей
device:<id> устройство отсутствует в полном device registry orphaned / device_missing
entity:<id> сущность найдена и entity.disabled_by != null ha_disabled / entity
entity:<id> сущность активна, но её родительское устройство деактивировано ha_disabled / device
entity:<id> сущность и родительское устройство активны active
entity:<id> сущность отсутствует в полном entity registry orphaned / entity_missing
virtual всегда текущая логика виртуального объекта
любой binding с marker.removed: true всегда tombstone; объект не строится
HA-binding, полный registry недоступен, но сокращённый registry/live-state подтверждает активность limited fallback active
HA-binding, полный registry недоступен и нет достаточного свидетельства limited fallback unverified / registry_unavailable

Устройство без сущностей не считается деактивированным автоматически: HA допускает device registry entries, представляющие сервисы или хабы без сущностей. Для all_entities требуется, чтобы в полном реестре была хотя бы одна дочерняя сущность и ни одной активной.

7.4. Частично деактивированное устройство

Если у устройства деактивирована только часть сущностей:

  • само устройство остаётся активным;
  • деактивированные сущности полностью исключаются из runtime-набора;
  • primary и функциональная роль пересчитываются только по активным сущностям;
  • сохранённые ссылки на деактивированные сущности не удаляются из marker;
  • после активации сущности ссылки снова начинают работать автоматически.

House Plan не пытается угадать, является ли деактивированная сущность «главной». Если пользователь хочет убрать весь объект, нужно деактивировать само устройство в HA либо скрыть/удалить его на плане.

8. Runtime-модель и приоритеты

Для построенного DevItem нужны отдельные runtime-поля, например:

interface DevItem {
  userHidden?: boolean;       // marker.hidden === true
  hidden?: boolean;           // effectiveHidden для совместимости рендера
  bindingStatus: HaBindingStatus;
  entities: string[];         // только активные runtime-сущности
  allEntities?: string[];     // реестровые метаданные/диагностика, без state/action
}

Формула:

const userHidden = marker?.hidden === true;
const forcedHidden = bindingStatus.kind === 'ha_disabled';
const effectiveHidden = userHidden || forcedHidden;

Приоритеты жизненного цикла:

  1. removed — не строить;
  2. orphaned — отдельное существующее/будущее управление потерянной привязкой;
  3. unverified — безопасно не рендерить и не использовать без disabled-ghost/баннера;
  4. ha_disabled — принудительно скрыть без записи в конфиг;
  5. userHidden — скрыть по сохранённому выбору пользователя;
  6. active — обычное отображение.

allEntities допускается использовать только для названия, иконки, текста диагностики и ссылки «Открыть в HA». State, действия и агрегаты берутся только из entities.

9. Поведение на плане

Состояние Просмотр Киоск Редакторы плана/подложки Редактор устройств, обычный режим Редактор устройств + «показать скрытые»
active, hidden: false да да по текущим правилам да да
active, hidden: true нет нет нет нет пользовательский ghost
ha_disabled, hidden: false нет нет нет нет HA-disabled ghost
ha_disabled, hidden: true нет нет нет нет HA-disabled ghost; после активации станет пользовательским ghost
unverified нет нет нет нет нет; limited-клиент не изображает ложный disabled-ghost
removed: true нет нет нет нет нет

Таблица описывает уже сохранённые marker-привязки. Деактивированный auto-discovered HA device без marker не строится даже как ghost: у него нет отдельного объекта плана, которым нужно управлять. После активации он снова проходит обычное автообнаружение; если для него уже сохранён layout по тому же ID, применяется существующий механизм восстановления позиции.

Деактивированный ghost:

  • нужен только для управления уже сохранённой привязкой;
  • визуально отличается от обычного пользовательски скрытого объекта;
  • не показывает live-state, цифры, морфинг иконки, жёлтую подложку, alarm или activity;
  • доступен кликом для открытия настроек marker;
  • не участвует в content frame, но для сохранённого auto-layout резервирует прежний слот, чтобы соседние устройства не прыгали;
  • получает локализованные title и aria-label с причиной.

Рекомендуемый визуал: серый полупрозрачный marker с пунктирной рамкой и небольшим mdi:power-plug-off-outline/аналогичным badge. Цвет пользовательски скрытого ghost не переиспользуется.

Локальный переключатель редактора рекомендуется переименовать:

  • RU: «Скрытые и деактивированные»;
  • EN: “Hidden and disabled”.

Он не меняет конфигурацию и действует только в текущей вкладке.

10. Влияние на данные и функции плана

Деактивированное в HA устройство ведёт себя как отсутствующий runtime-источник, но его конфигурация сохраняется.

Подсистема Поведение ha_disabled
Значок и подложка устройства не рендерятся вне служебного ghost
Жёлтая индикация работы отсутствует
Пульсация/activity/event отсутствует; runtime-история очищается
Alarm/critical indication отсутствует
Glow устройство и его сущности исключены
Заливка «Свет по источникам» исключено
resolvedLightSources(room) исключено централизованно
Карточка комнаты и групповые controls исключено
Статистика света комнаты исключено
Температура/влажность, среднее по комнате деактивированные сущности исключены
Явно выбранный источник температуры/влажности значение —; не подменять другим источником молча
Zigbee/LQI и заливка по сигналу исключено, в отличие от обычного marker.hidden
Счётчик устройств не учитывать
Автоматические группы устройств/света исключено из runtime-состава
Нажатие, переключение, cover, more-info действие не выполняется
Сохранённые controls других marker ссылка сохраняется, но runtime-цель исключается
Проёмы с lock/contact ссылка сохраняется; badge, state и действие временно отсутствуют
Live text {entity} — по текущему правилу мёртвой сущности; шаблон сохраняется
Вложения, PDF, URL, описание сохраняются; доступны через настройки служебного ghost
Координаты и auto-grid сохраняются; соседние auto-grid marker не должны перестраиваться из-за временной деактивации
Content frame деактивированный marker не расширяет рамку контента
Пылесос: puck и текущий/предыдущий trail puck и оба следа не рендерятся; загруженная история остаётся сохранённой, но ghost её не показывает

Обычное пользовательское скрытие сохраняет текущую согласованную семантику из docs/FILTERING.md. В частности, существующее решение учитывать hidden-устройства в некоторых комнатных агрегатах не переносится на ha_disabled: HA-деактивация сильнее presentation-флага.

11. Диалог настроек устройства

11.1. Индикация

В верхней части содержимого диалога показывается неблокирующий banner:

  • RU, device: «Устройство деактивировано в Home Assistant и скрыто с плана.»
  • RU, entity: «Сущность деактивирована в Home Assistant и скрыта с плана.»
  • RU, all entities: «У устройства нет активных сущностей Home Assistant, поэтому оно скрыто с плана.»
  • EN: эквивалентные строки без привязки к конкретной интеграции.

Если реестровая запись существует, «Открыть в HA» остаётся доступной. Для device-binding она ведёт на /config/devices/device/<device_id>. Для disabled entity-binding more-info не используется: при наличии родительского device_id открывается страница устройства, иначе — страница настройки записи entity registry, поддерживаемая текущей версией HA. Кнопка не должна открывать пустой more-info деактивированной сущности. Удаление с плана также остаётся доступным.

11.2. Кнопка «Показать»

Кнопка остаётся кликабельной, потому что по требованию нужен явный ответ на попытку, а не немая disabled-кнопка.

При клике:

  1. статус binding повторно проверяется по самой свежей доступной версии реестров;
  2. если он ha_disabled, состояние формы и marker.hidden не меняются;
  3. показывается toast с role="alert":
    • RU: «Деактивированное в Home Assistant устройство нельзя показать на плане. Сначала активируйте его в Home Assistant.»
    • EN: “A device disabled in Home Assistant cannot be shown on the plan. Enable it in Home Assistant first.”
  4. фокус остаётся на кнопке;
  5. сохранение других настроек остаётся возможным.

Для точной entity-привязки текст заменяет «устройство» на «сущность».

Если marker.hidden: true был установлен до деактивации, заблокированная попытка «Показать» не очищает его. После повторной активации marker остаётся скрытым до отдельного явного показа пользователем. Это сохраняет старое пользовательское намерение.

11.3. Rebind

Разрешается перепривязать сохранённый marker с деактивированного объекта на активное устройство/сущность. Статус и доступность «Показать» пересчитываются по текущему выбранному binding ещё до сохранения.

Если выбран новый активный binding, пользователь может показать marker и сохранить изменения обычным способом. Старый binding при этом обрабатывается по существующим правилам замены/защиты от дублей.

11.4. Удаление

«Удалить» работает по существующим правилам и с подтверждением:

  • HA-binding заменяется tombstone;
  • layout, runtime activity и plan-level данные очищаются;
  • последующая активация в HA не возвращает удалённый marker;
  • активированный binding снова доступен в «Добавить устройство» и может быть добавлен заново.

12. Добавление новых устройств

  1. Деактивированные device- и entity-binding не показываются среди обычных кандидатов добавления.
  2. Устройство, у которого все известные сущности деактивированы, также не показывается.
  3. Частично деактивированное устройство показывается один раз как активное; деактивированные дочерние сущности не предлагаются как отдельные активные binding.
  4. Текущая сохранённая деактивированная привязка остаётся видимой в собственном диалоге как выбранное значение с пометкой «деактивировано», даже если исключена из общего dropdown.
  5. Если кандидат деактивировался после выбора, но до «Сохранить», сохранение новой привязки блокируется с тем же сообщением. Marker не создаётся.
  6. Если кандидат снова активирован, dropdown и доступность сохранения обновляются без перезагрузки страницы.
  7. Если существует removed: true, но binding сейчас деактивирован, он не предлагается для повторного добавления до активации.

Это решает сценарий двух одинаковых устройств в HA: рабочий экземпляр доступен для добавления, деактивированный — нет.

13. Жизненный цикл

13.1. Карточка загружается, binding уже деактивирован

  • не должно быть краткого показа marker из кеша или memo-снимка;
  • сохранённый marker строится как служебный HA-disabled объект;
  • в режиме просмотра и киоске он не рендерится;
  • в редакторе устройств доступен только через «Скрытые и деактивированные»;
  • plan-level вклад устройства равен нулю.

Если актуальные реестры ещё не загружены, нельзя считать binding активным по старому state. До определения статуса применяется безопасное текущее loading-поведение без интерактивного marker.

13.2. Деактивация во время открытой карточки

На первом согласованном обновлении registry:

  1. marker исчезает из всех обычных режимов;
  2. отменяется активный drag/pointer gesture без сохранения новой координаты;
  3. закрывается открытая more-info карточка этого marker либо переводится в неинтерактивное состояние без stale-actions; предпочтительно закрыть;
  4. открытый диалог настроек marker остаётся открыт, получает banner и блокировку «Показать»;
  5. очищается activity/event runtime для binding;
  6. пересчитываются Glow, light fill, room stats, climate, LQI, alarms, controls и счётчики;
  7. автоматическая конфигурационная запись не выполняется.

Уже отправленный в HA service call отменить невозможно. После получения факта деактивации новые действия не отправляются.

13.3. Активация того же binding

Если ID в реестре не изменился:

  • marker.hidden !== true — marker автоматически возвращается на прежнее место со всеми настройками;
  • marker.hidden === true — marker остаётся пользовательски скрытым и доступен как обычный hidden ghost;
  • marker.removed === true — marker не возвращается;
  • состояние unknown/без state сразу после активации обрабатывается по существующей логике unavailable/off, но не как disabled;
  • первый state после активации становится baseline и не создаёт ложный event/ripple/alarm transition;
  • reactivation не помечается как «новое устройство» и не создаёт красную точку для уже сохранённого binding.

Это правило относится только к уже сохранённому marker. Деактивированный auto-device, который House Plan никогда не строил и не добавлял в known_devices, после первой активации проходит обычный auto-discovery как новое устройство и может получить стандартную красную точку. Старый device_id сам по себе не является основанием подавлять new-device flow.

Если интеграция создала новый device_id/entity_id, это новый binding. Старый остаётся orphaned; автоматическое сопоставление по имени, модели или unique_id не выполняется.

13.4. Несогласованный порядок обновлений HA

HA может обновить device registry, entity registry и states не одним атомарным пакетом.

  • При деактивации ненулевой device.disabled_by сразу имеет приоритет над старыми states и якобы активными дочерними сущностями.
  • Исчезновение state до обновления registry не считается деактивацией.
  • При активации marker возвращается только когда родительское устройство активно и для существующего набора сущностей есть хотя бы одна активная сущность либо устройство легитимно не имеет сущностей.
  • Если device уже активирован, но все его сущности ещё помечены disabled, сохраняется ha_disabled / all_entities; это предотвращает мигание marker.
  • При entity-binding родительский device и сама сущность должны быть активны одновременно.

13.5. Изменение статуса при открытом диалоге

  • Проверка выполняется не только при открытии диалога, но и перед Show и Save.
  • Если пользователь нажал «Показать», а binding деактивировался до Save, изменение marker.hidden не сохраняется; остальные валидные поля могут быть сохранены.
  • Если binding активировался при открытом диалоге, banner исчезает, Show снова работает.
  • Если пользователь успешно показал активный marker, а HA деактивировал его сразу после этого, сохранённый hidden: false остаётся законным пользовательским намерением; marker временно скрывается и автоматически вернётся после активации.

14. Особые случаи

14.1. off, unknown, unavailable, отсутствующий state

Все эти состояния относятся к активной реестровой записи и сохраняют текущий визуальный контракт продукта. Они не показывают новый disabled-ghost и не блокируют «Показать».

14.2. hidden_by в HA

hidden_by влияет только на видимость в интерфейсе Home Assistant. Такая сущность может быть primary или явной целью House Plan и не исключается данным ТЗ.

14.3. Деактивация одной вспомогательной сущности

Она исключается из всех резолверов, но устройство остаётся. Пример: отключённый диагностический RSSI не скрывает активный клапан.

14.4. Деактивация основной сущности при активной диагностике

House Plan не объявляет весь device деактивированным без device.disabled_by. Он строит роль по оставшимся активным сущностям. Нельзя вводить эвристику по словам Power/Main/State: она вновь создаст ошибки для разных интеграций.

14.5. Все сущности деактивированы, устройство формально активно

Binding принудительно скрывается с причиной all_entities. Это практически неработоспособный device-binding и не должен жить на stale state.

14.6. Устройство удалено из интеграции

Отсутствие записи в полном registry — orphaned, а не ha_disabled. Нельзя обещать ссылку «активировать в HA», если объекта больше нет. Удаление marker остаётся доступно по существующему механизму управления потерянными привязками.

14.7. Интеграция удаляет запись вместо disabled_by

Официальные рекомендации HA допускают интеграции, которые при выключении опции удаляют сущность из registry. House Plan увидит orphaned, а не disabled. Автоматически считать любое исчезновение деактивацией нельзя: это скроет реальные поломки и ID churn. В будущем можно сделать отдельное ТЗ на orphaned bindings.

14.8. Пользователь без права редактирования или полного registry

Runtime-деактивация не требует записи в конфигурацию. Если read-only пользователь получает полный registry, поведение полностью совпадает с admin-клиентом: marker отсутствует и не влияет на данные. Если registry-команды запрещены, применяется limited fallback из §7.1: подтверждённый active работает, неподтверждённый binding безопасно не рендерится и не влияет на данные, но не называется деактивированным. Служебный disabled-ghost гарантируется только клиенту с авторитетным registry и доступным редактором устройств.

14.9. Несколько открытых клиентов

Клиенты независимо вычисляют статус из одного HA registry. Автоматических config writes нет, поэтому деактивация не создаёт конфликтов ревизий. Обычные пользовательские Hide/Delete/Rebind продолжают использовать существующую серверную защиту.

14.10. Static houseplan-space-card

Статическая карточка применяет тот же центральный статус и не рендерит ha_disabled. Если live effects в ней и так отключены, фильтрация всё равно обязательна для marker, counts и рамки контента.

14.11. Смена area в HA во время деактивации

Сохранённая room/space-привязка и координаты не переписываются только из-за деактивации. После активации применяется существующий приоритет явных настроек marker над автоназначением area.

14.12. Сохранённый control с деактивированной целью

Raw ID не удаляется и не исчезает из формы. В UI он помечается как деактивированный; действие и агрегация временно отключены. После активации control восстанавливается без повторного сохранения marker.

14.13. Пылесосы и серверная запись следов

Деактивированный vacuum-binding исключается тем же центральным resolver: marker, puck, live path, текущий и предыдущий серверный trail не рендерятся, а клиентский _vacRt очищается. houseplan/trail/get может продолжать возвращать сохранённую историю — это допустимо, потому что фильтрация выполняется по активному marker до рендера.

TrailRecorder на backend в рамках задачи не меняется и не начинает читать device/entity registry. Деактивированная сущность перестаёт поступать в state machine, поэтому новые точки естественно не записываются. Существующая история не удаляется при временной деактивации и снова доступна после активации того же binding; удаление с плана по-прежнему очищает её отдельной командой.

15. Архитектура реализации

15.1. Один резолвер

Создать чистый модуль/набор функций, например src/ha-binding-status.ts, либо явно выделенный раздел src/devices.ts:

isRegistryEntryEnabled(entry): boolean
enabledEntitiesForDevice(hass, deviceId): string[]
resolveHaBindingStatus(hass, binding): HaBindingStatus

Все consumers получают уже отфильтрованный runtime-набор. Нельзя повторять disabled_by == null в десятках мест.

15.2. Построение устройств

buildDevices должен:

  • строить auto-discovered список только из активных binding;
  • для сохранённого деактивированного marker создавать служебный DevItem, чтобы он был управляем в редакторе;
  • передавать в DevItem.entities только активные сущности;
  • сохранять отдельно реестровые metadata IDs для подписи/диагностики;
  • не создавать marker и не запускать filter seeder только из-за runtime-деактивации;
  • не терять marker.controls, is_light, attachments и другие настройки.

15.3. Инвалидация registry-кеша

Текущей сигнатуры вида «количество devices : количество entities : количество areas» недостаточно: переключение disabled_by обычно не меняет количества записей.

Инвалидация обязана учитывать как минимум:

  • device.id + device.disabled_by;
  • entity.entity_id + entity.device_id + entity.disabled_by;
  • появление/исчезновение записей;
  • актуальную ревизию/identity registry, если HA frontend предоставляет её надёжно.

Реализация может подписаться на registry-update либо вычислять стабильную компактную сигнатуру. Она не должна сортировать и сериализовать весь реестр на каждый обычный state tick, если ссылки/ревизии registry не менялись.

Полное представление обоих реестров, in-flight запросы, debounce обновления и подписки на entity_registry_updated/device_registry_updated принадлежат модульному page-level cache, по одному на HA connection. Все экземпляры houseplan-card и houseplan-space-card на странице разделяют этот cache; каждый экземпляр только подписывается на его revision и отписывается при disconnect. Несколько карточек не должны создавать несколько полных registry fetch или WS-подписок.

Последний авторитетно вычисленный статус сохранённых binding хранится отдельно от конфигурации в ограниченном localStorage runtime-кеше. В нём нет marker metadata и state values. ha_disabled можно использовать на первом кадре как forced-hidden; cached active требует текущего сокращённого registry/state-свидетельства. Успешный полный fetch обновляет кеш, reactivation перезаписывает старый disabled-результат. Ошибка/отказ доступа не затирает последний авторитетный результат.

15.4. Runtime consumers

Обязательные точки перехода на центральный active-набор:

  • entitiesByDevice;
  • resolvedDeviceStateEntities и primaryEntity;
  • resolvedLightSources;
  • temperature/humidity/LQI;
  • alarm/critical и activity transition;
  • cover/tap/more-info/service calls;
  • controls и room/group actions;
  • room cards, stats и fills;
  • opening lock/contact;
  • live text;
  • add/binding pickers;
  • full card и houseplan-space-card;
  • content frame и device counters.
  • vacuum puck, клиентские и серверные trails.

Каждая service action дополнительно проверяет текущий статус непосредственно перед вызовом, чтобы stale DOM не мог отправить команду деактивированной цели.

15.5. Runtime cleanup

При переходе active -> ha_disabled очистить:

  • activity/event timers;
  • cached visual samples;
  • pending click/long-press confirmation для marker;
  • активный drag/pointer capture;
  • открытый stale more-info;
  • вычисленные light/climate/LQI caches, зависящие от binding.

При ha_disabled -> active сначала создать baseline текущих states, затем разрешить новые transitions.

Backend TrailRecorder в этот список не входит: его state subscription уже естественно останавливается вместе с disabled entity.

15.6. I18n и стили

Добавить RU/EN ключи для:

  • трёх причин banner;
  • blocked Show toast для device/entity;
  • подписи деактивированного кандидата;
  • переключателя «Скрытые и деактивированные»;
  • tooltip/aria-label служебного ghost.
  • limited-registry пояснения для диагностики/саппорта без ложного текста «деактивировано».

Добавить отдельный styling hook/data attribute, например:

data-binding-status="ha-disabled"
data-disabled-reason="device|entity|all-entities"

Это не должно переиспользовать .unavail как единственный признак: unavailable — другой продуктовый статус.

15.7. Поддержка touch

Классификация по docs/TOUCH-SUPPORT.md:

  • View и киоск: поддерживается полностью. Деактивированный marker обязан исчезнуть, перестать влиять на данные и корректно восстановиться после активации на телефонах, планшетах и настенных панелях.
  • Редактор устройств: best effort / допускается осознанная деградация. Управление ghost, rebind и точная работа длинного диалога гарантируются в desktop-браузере. На touch они могут быть менее удобны или частично недоступны.
  • Если редактор всё же показывает действие на touch, safety-инварианты сохраняются: disabled binding нельзя показать или задействовать, Delete требует подтверждения, а конфигурация не повреждается.
  • Реализация не должна усложнять центральный resolver или View только ради touch-паритета редактора.

16. Совместимость и миграция

  • Формат marker не меняется.
  • Backend validation не меняется.
  • Версия модели не повышается.
  • Существующие hidden: true, hidden: false и removed: true сохраняют смысл.
  • Старые конфигурации начинают корректно реагировать на disabled_by сразу после обновления frontend.
  • Если старая версия House Plan ранее ошибочно сохранила пользовательский hidden: true, новая версия не может доказать, что это было автоматическое скрытие, и не должна его очищать.
  • Деактивация не инициирует _saveConfig, layout write или Optimize Plans.
  • Runtime-кеш статусов не является частью config/layout schema, ограничен по размеру, может быть безопасно удалён и не требует миграции серверной модели.
  • Frontend diagnostics показывает количества ha_disabled и unverified, доступность полного registry и возраст последней успешной синхронизации. Backend diagnostics.py/system_health.py продолжают считать сохранённые marker по конфигу и не пытаются вычислять runtime-статус клиента.

17. Тестовый план

17.1. Unit: статус binding

  1. device active, одна активная entity -> active.
  2. device.disabled_by = 'user' при живом stale state -> ha_disabled / device.
  3. entity active, parent device disabled -> ha_disabled / device.
  4. entity.disabled_by = 'integration' -> ha_disabled / entity.
  5. неизвестное ненулевое disabled_by -> disabled.
  6. часть сущностей disabled -> device active, runtime-набор содержит только enabled.
  7. все дочерние сущности disabled -> ha_disabled / all_entities.
  8. active device с нулём сущностей -> не all_entities.
  9. отсутствующий state при активном registry -> active/unavailable, не disabled.
  10. отсутствующая registry entry -> orphaned.
  11. hidden_by без disabled_by -> active.
  12. virtual -> вне HA-disabled логики.
  13. limited registry + сокращённая active entry -> active.
  14. limited registry + точный live-state без entity registry entry -> active entity-binding.
  15. limited registry + нет registry/state evidence -> unverified, не disabled/orphaned.
  16. cached disabled сохраняет forced-hidden до перепроверки; cached active без evidence не показывает marker.

17.2. Unit: построение и агрегаты

  1. Не сохранённый disabled auto-device не строится как обычный marker.
  2. Сохранённый disabled marker строится только как управляемый forced-hidden DevItem.
  3. marker.hidden не мутируется.
  4. Partial-disabled primary не выбирает disabled entity.
  5. Disabled controls/light/climate/LQI/alarm исключены.
  6. Raw references сохраняются.
  7. Tombstone имеет приоритет и не строится даже ghost.
  8. Изменение только disabled_by инвалидирует device build.
  9. Две карточки на одной странице используют один registry fetch/набор подписок.
  10. Disabled vacuum не рендерит puck/trails; серверная история не удаляется.
  11. Никогда не виденный disabled auto-device не попадает в known_devices; после активации проходит обычный new-device flow.

17.3. Browser smoke

  1. Видимый сохранённый marker -> установить device.disabled_by -> marker исчезает без config write.
  2. Проверить отсутствие Glow, жёлтой подложки, stats, controls, alarm и count.
  3. В редакторе включить «Скрытые и деактивированные» -> увидеть distinct ghost.
  4. Нажать «Показать» -> увидеть toast; marker.hidden не меняется.
  5. Проверить «Открыть в HA», редактирование metadata и Delete.
  6. Снять disabled_by -> marker возвращается в те же координаты.
  7. Повторить при исходном marker.hidden: true -> после активации остаётся hidden.
  8. Повторить при removed: true -> не возвращается.
  9. Деактивировать во время drag, more-info и marker dialog.
  10. Деактивировать между выбором binding и Save в Add.
  11. Проверить частично деактивированное устройство.
  12. Проверить entity-binding и disabled parent device.
  13. Проверить kiosk, plan/decor editor и houseplan-space-card.
  14. Проверить reactivation без ложного activity pulse.
  15. Проверить, что auto-grid соседи не прыгают.
  16. Расширить registry-стабы demo.html полями disabled_by и режимом отказа registry WS.
  17. Проверить admin/full-registry и read-only/limited-registry сценарии.
  18. Проверить vacuum puck, live trail, server current/previous history.
  19. Проверить, что две full cards и houseplan-space-card не дублируют registry fetch/subscription.

17.4. Ручная проверка в настоящем HA

  1. Отключить устройство через UI HA.
  2. Отключить отдельную сущность пользователем.
  3. Проверить сущность, disabled by integration/config entry.
  4. Активировать обратно без перезагрузки карточки.
  5. Повторить в двух одновременно открытых клиентах.
  6. Проверить пару одинаково названных устройств, где одно деактивировано: в Add должен быть только активный кандидат.

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

Функция считается готовой, если одновременно выполняются все условия:

  1. Деактивированный в HA сохранённый binding нигде не отображается как обычное устройство.
  2. Он не влияет ни на один визуальный эффект, показатель, агрегат или действие плана.
  3. Конфигурация, координаты и пользовательский hidden-флаг не изменяются автоматически.
  4. Он управляем только как distinct ghost в редакторе устройств.
  5. Попытка «Показать» выдаёт локализованное сообщение и не меняет данные.
  6. Повторная активация того же ID автоматически восстанавливает marker, если пользователь не скрывал и не удалял его.
  7. Пользовательски скрытый marker после активации остаётся скрытым.
  8. Удалённый marker после активации не возвращается.
  9. Disabled entity никогда не становится primary и не используется косвенно через controls, light, climate, LQI или alarm.
  10. unknown, unavailable, отсутствие state и hidden_by не ошибочно распознаются как деактивация.
  11. Изменение disabled_by с неизменным размером registry немедленно обновляет карточку.
  12. Добавление нового marker для disabled binding невозможно.
  13. Клиент без доступа к полному registry не создаёт ложных disabled/orphaned статусов и не выполняет действие по неподтверждённой привязке.
  14. Деактивированный пылесос не показывает puck/следы; backend recorder не требует изменений.
  15. На первом кадре warm/cached boot последний известный disabled marker не вспыхивает как активный.
  16. Несколько карточек на странице разделяют один полный registry cache и подписки.

19. Решения, заложенные в это ТЗ и требующие подтверждения владельца

  1. Повторная активация автоматически возвращает marker, если marker.hidden !== true.
  2. HA-disabled полностью исключается из данных плана, включая LQI и registry-wide климат; это намеренно строже обычного пользовательского Hide.
  3. Активное device с хотя бы одной активной сущностью не считается деактивированным, даже если его предполагаемая «главная» сущность выключена.
  4. Активное device, у которого все известные сущности disabled, считается ha_disabled / all_entities.
  5. Blocked Show не очищает старый пользовательский hidden: true. После активации устройство всё ещё нужно показать вручную.
  6. Auto-grid сохраняет слот деактивированного сохранённого marker, чтобы временная деактивация не перестраивала расположение соседей.
  7. Отсутствующий registry ID не считается disabled. Это отдельная orphaned-проблема и не маскируется данным функционалом.
  8. При отсутствии доступа к полному registry используется unverified, а не ложный disabled/orphaned; безопасность данных и действий важнее временной полноты View.
  9. Последний известный disabled-статус кешируется отдельно от конфигурации; cached active без актуального evidence не авторизует рендер или действие.
  10. Никогда не виденный auto-device после активации считается новым; уже сохранённый binding — нет.

20. Сопутствующая документация реализации

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

  • docs/FILTERING.md — переключатель «Скрытые и деактивированные», различия hidden/disabled/unverified;
  • docs/TESTING.md и docs/TESTING-DEMO.md — registry stubs, admin/limited и vacuum-сценарии;
  • docs/ARCHITECTURE.md — page-level registry cache и центральный binding resolver;
  • диагностический раздел пользовательской документации — как отличить disabled от limited registry.

21. Источники Home Assistant

  • Entity registry and disabling entities — disabled_by != None означает, что сущность не добавляется в Home Assistant; перечислены причины user/integration/config entry.
  • Device registry — устройство является registry entry, может иметь disabled_by, а также может быть зарегистрировано без сущностей.
  • WebSocket API — актуальные API HA отдельно фильтруют disabled entities, поэтому сокращённое представление нельзя использовать для отличия disabled от missing; нужен полный registry.