From 8c0098ebf58c3163dc7cd057b2c88893091697c1 Mon Sep 17 00:00:00 2001 From: Matysh Date: Mon, 31 Aug 2026 08:12:40 +0300 Subject: [PATCH] docs: research Zigbee topology overlay Issue: #54 User-Visible: no --- docs/specs/054-zigbee-topology-overlay.md | 444 +++++++++++++++++++--- 1 file changed, 394 insertions(+), 50 deletions(-) diff --git a/docs/specs/054-zigbee-topology-overlay.md b/docs/specs/054-zigbee-topology-overlay.md index 98f29eb1..0d26ac45 100644 --- a/docs/specs/054-zigbee-topology-overlay.md +++ b/docs/specs/054-zigbee-topology-overlay.md @@ -1,74 +1,418 @@ -# ТЗ #54 — Диагностический Zigbee topology overlay +# Исследование и ТЗ #54 — диагностический слой Zigbee-топологии - Issue: https://github.com/Matysh/houseplan-card/issues/54 -- Приоритет: P2 -- Статус ТЗ: research + adapter contract; provider APIs проверяются до кода +- Приоритет: P3 +- Статус: **Stage 0 research завершён; реализация не начиналась** +- Решение Stage 0: **GO с условиями и поэтапным вводом провайдеров** +- Дата проверки контрактов: 2026-08-31 -## Цель +Проверенные upstream-состояния: -По явному запросу показать связи ZHA/Zigbee2MQTT между уже размещёнными -устройствами поверх реальной геометрии, не превращая House Plan в редактор сети. +| Компонент | Проверено | +|---|---| +| Home Assistant Core | `2026.8.3` и `dev@6ad726ba56d517e56536cdd6fa2ba9f358bbc0ef` | +| Home Assistant Frontend | `dev@6b7d3871754dada4d599e4fecc0d5ca0442cf22b` | +| Zigbee2MQTT | `2.13.0` и `master@fcbb7ff44bdc05a16e95d3472a81d110646cc17b` | +| House Plan | `dev@6bf39ee9a7ecbc62dcc8b999af3c23b6d877c5a0` | -## Stage 0 — capability research +## 1. Сценарий и продуктовая рамка -На поддерживаемых версиях HA зафиксировать официальные/фактические API: +Персона — Enthusiast/Power User, поверхность — полная карточка House Plan в +режиме View, момент — диагностика нестабильного Zigbee-устройства. Пользователь +включает временный слой и видит наблюдаемые Zigbee-связи поверх реального плана, +не переходя в редактор и не изменяя сеть. -- ZHA topology/neighbour source, permissions, direction и LQI semantics; -- Zigbee2MQTT networkmap request/response через HA MQTT integration либо - документированную exposed entity; -- соответствие IEEE ↔ HA device registry `connections`; -- timeout/rate limits и поведение sleeping end devices. +До: House Plan показывает LQI устройства и комнаты, но не отвечает, через какой +router идёт наблюдаемая связь и где он находится относительно стен. -Неподтверждённые command/topic не hardcode-ятся. Каждый provider имеет fixture -с обезличенными ids и дату/версию проверенного контракта. +После: для размещённых устройств текущего пространства видны наблюдаемые связи и +их LQI; связи в другие пространства представлены честной навигационной ссылкой, +а не геометрически ложной линией между независимыми планами. -## Normalized model +Функция служит J7 из `docs/SCOPE.md`. Это read-only диагностика, не network +manager: pairing, reconfigure, bind/unbind, удаление устройств и изменение +маршрутов не входят в scope. + +## 2. Краткий вердикт Stage 0 + +| Часть | Вердикт | Основание и ограничение | +|---|---|---| +| Общая модель и пространственный слой | **GO** | Текущий House Plan уже имеет полный registry snapshot, exact bindings, позиции и пространство каждого live marker. Слой можно загружать лениво и держать pointer-transparent. | +| ZHA: чтение snapshot | **GO, высокая уверенность** | Admin-only `zha/devices` возвращает `device_reg_id`, IEEE, роль, `neighbors`, `routes` и LQI. Это тот же источник, который использует штатная ZHA visualization. | +| ZHA: принудительный свежий scan | **условный GO** | `zha/topology/update` существует и admin-only, но в проверенной реализации только запускает background scan и не отправляет result/completion. Нельзя честно показать завершение или определить неизменившийся результат. | +| Zigbee2MQTT: свежий scan | **GO с настройкой** | Документированный MQTT request/response возвращает raw nodes/links. Нужен явно заданный base topic для каждой Z2M-инстанции; надёжно вывести его из HA registry нельзя. | +| Одинаковая семантика данных | **GO с нормализацией** | Оба провайдера дают наблюдаемые directional neighbor records, но completeness, route flags и freshness различаются. Нельзя выдавать их за гарантированный фактический маршрут каждого пакета. | +| Связи между этажами | **GO без межплановой линии** | Пространства имеют независимые viewBox/камеры. Нужны endpoint indicator, список связей и переход к удалённому marker. | + +Итог: задачу реализовать можно. Нельзя обещать полную паритетность двух +провайдеров или «живую карту маршрутов». Выпускать следует provider capability, +а не включать хрупкий общий режим при частично доступном источнике. + +## 3. Подтверждённые provider-контракты + +### 3.1 ZHA + +Источник: + +- Home Assistant Core: + `homeassistant/components/zha/websocket_api.py`; +- Home Assistant Frontend: `src/data/zha.ts` и + `zha-network-data.ts` штатной network visualization; +- zigpy: `zigpy/topology.py` и `zigpy/application.py`. + +Подтверждено: + +1. `zha/devices` требует admin и отдаёт массив устройств. У каждого устройства + есть `ieee`, `device_reg_id`, `device_type`, `neighbors`, `routes`; neighbor + несёт `ieee`, `nwk`, `relationship` и `lqi`. +2. `device_reg_id` даёт прямое соответствие HA Device Registry без эвристики по + имени или entity id. +3. Штатный HA frontend строит граф именно из этих `neighbors`/`routes`, хранит + два directional LQI для пары при наличии двух записей и использует route + только вместе с реальным neighbor next hop. +4. zigpy по умолчанию включает периодический topology scan с периодом четыре + часа. Поэтому `zha/devices` обычно является дешёвым чтением кеша, а не новым + radio scan. Данные могут быть неполными и старыми. +5. `zha/topology/update` также требует admin, но на проверенных `2026.8.3` и + текущем `dev` запускает `topology.scan()` через background task и не отправляет + WebSocket result. Клиент не получает completion и timestamp результата. + +Следствие для реализации: + +- безопасный первый ZHA capability — **прочитать кешированный snapshot**; +- кнопка «Пересканировать сеть» не должна использовать fire-and-forget как будто + это завершённый request; +- свежий scan допускается после появления completion-aware upstream API либо в + отдельном изолированном compatibility adapter, который await-ит ZHA topology + scan и fail-closed при несовместимой версии. Этот вариант повышает стоимость + поддержки и не является условием первого ZHA этапа; +- UI должен писать «Данные ZHA прочитаны …», а не утверждать неизвестное время + фактического сканирования. + +### 3.2 Zigbee2MQTT + +Источник: + +- документация MQTT API: + `BASE/bridge/request/networkmap` → `BASE/bridge/response/networkmap`; +- `lib/extension/networkMap.ts` и `lib/types/api.ts` Zigbee2MQTT. + +Подтверждено: + +1. Request `{"type":"raw","routes":false,"transaction":"…"}` возвращает + raw `nodes[]` и `links[]`; `transaction` позволяет сопоставить конкурентные + request/response. +2. Node содержит IEEE, friendly name, роль и scan failures. Link содержит + `source`, `target`, directional `lqi`, relationship и необязательные routes. +3. Scan опрашивает coordinator и routers, делает паузу между устройствами и + retry после ошибки. Документация предупреждает о снижении отзывчивости сети + и длительности от 10 секунд до 2 минут; запускать его разрешено только явно. +4. `routes:false` достаточен для spatial links и уменьшает нагрузку: route table + не нужна, чтобы показать наблюдаемый neighbor edge. +5. Disabled в конфигурации Z2M и Green Power устройства исключаются самим + provider; отдельные router failures приходят в snapshot и не должны ломать + весь граф. +6. HA MQTT предоставляет admin-only WebSocket subscription `mqtt/subscribe`, а + `mqtt.publish` является штатным action. Adapter может работать через HA, + не подключая браузер напрямую к broker. + +Base topic: + +- default равен `zigbee2mqtt`, но он конфигурируемый и может содержать `/`; +- HA Device/Entity Registry не содержит надёжного исходного base topic; +- wildcard-поиск по всему broker недопустим из-за privacy, нагрузки и риска + прочитать чужие topics; +- поэтому для Z2M нужен список provider instances с явным `baseTopic`. + Начальное значение `zigbee2mqtt` проверяется чтением retained `bridge/info`; + несколько инстансов задаются отдельно. + +### 3.3 Соответствие Z2M node → HA device + +Zigbee2MQTT по умолчанию публикует HA device identifiers: + +- обычное устройство: `zigbee2mqtt_`; +- bridge/coordinator: `zigbee2mqtt_bridge_`. + +Adapter нормализует IEEE и ищет exact identifier в полном HA Device Registry. +Допустимые fallback идут только через exact Entity Registry owner того же HA +device. Friendly name, entity name и model не используются для автоматического +matching. User-overridden discovery identifiers могут оставить node unmatched; +это диагностируется, но не исправляется догадкой. + +## 4. Нормализованная модель + +Provider-specific payload не проходит в render. ```ts +type ZigbeeProvider = 'zha' | 'z2m'; + +type ZigbeeTopologyWarning = { + code: + | 'invalid_payload' + | 'duplicate_link' + | 'self_link' + | 'unmatched_device' + | 'ambiguous_placement' + | 'provider_scan_failure'; + nodeKey?: string; + detail?: string; +}; + type ZigbeeTopology = { - provider: 'zha' | 'z2m'; capturedAt: number; - nodes: Array<{ key: string; deviceId?: string; role: 'coordinator'|'router'|'end' }>; - links: Array<{ from: string; to: string; lqi?: number; direction: 'one'|'both'|'unknown' }>; - warnings: string[]; + provider: ZigbeeProvider; + instanceId: string; + obtainedAt: number; + freshness: 'provider-cache' | 'fresh-scan'; + nodes: ZigbeeTopologyNode[]; + links: ZigbeeTopologyLink[]; + warnings: ZigbeeTopologyWarning[]; +}; + +type ZigbeeTopologyNode = { + key: string; // provider instance + normalized IEEE + ieee: string; // never shown/logged by default + deviceId?: string; // exact HA Device Registry id + role: 'coordinator' | 'router' | 'end' | 'unknown'; + available?: boolean; + providerFailures?: string[]; +}; + +type ZigbeeDirectionalObservation = { + lqi?: number; // provider-native 0..255 + relationship?: string; + activeRoute?: boolean; +}; + +type ZigbeeTopologyLink = { + a: string; + b: string; + aToB?: ZigbeeDirectionalObservation; + bToA?: ZigbeeDirectionalObservation; }; ``` -Provider adapters живут backend-side либо в изолированном HA adapter; render -не знает API. Browser не подключается к MQTT напрямую и не хранит topology в -House Plan config. +Правила нормализации: -## Fetch/cache/security +- одинаковая unordered pair хранится один раз, две стороны не усредняются; +- один directional record не превращается в bidirectional; +- route destination не создаёт прямую линию: route только помечает реальный + neighbor next-hop; +- неизвестный/некорректный LQI остаётся `undefined`, а не `0`; +- duplicate и self links отбрасываются с warning; +- payload ограничивается по числу nodes/links и размеру до разбора. -- Overlay off по умолчанию; первый toggle запускает fetch с progress/cancel. -- Shared per-provider cache TTL 60 s (уточняется Stage 0), in-flight dedupe и - hard timeout. Toggle off отменяет UI ожидание, но безопасный backend request - может завершить cache. -- Refresh только явный или после TTL; HA state ticks не сканируют topology. -- Read permission проверяется backend. Ошибка/unsupported не ломает plan и - показывает localized status. +## 5. Mapping на House Plan marker -## Mapping и визуал +Для node сначала определяется exact HA `deviceId`, затем кандидаты среди live +House Plan markers этого HA device: -Node сопоставляется marker только через device registry/explicit entity owner. -На plan рисуются links, у которых оба конца имеют видимые live markers текущего -space. Остальные nodes доступны в summary «Не размещено N», но не рисуются. +1. единственный видимый marker с binding `device:`; +2. если его нет — единственный видимый entity marker, чья entity принадлежит + этому device; +3. несколько равноправных markers — `ambiguous_placement`, node не рисуется; +4. hidden, removed и HA-disabled markers не являются drawable endpoints; +5. rebind/перемещение/смена пространства пересчитывают mapping по существующему + snapshot без нового Zigbee scan. -Edges — pointer-transparent отдельный layer. Цвет/opacity/thickness отражают -нормализованный LQI только при сопоставимой шкале provider; unknown — нейтральный -пунктир. Direction optional arrow только при достоверных данных. Coordinator -и router имеют legend. Geometry стен не интерпретируется как причина сигнала. +Так исключается линия к случайной сущности составного устройства. Unmatched, +ambiguous и hidden nodes входят в summary с раздельными причинами. -## Multiple spaces и lifecycle +Coordinator часто не размещён на плане. Это не повод создавать автоматический +marker или фиктивную координату: его связи остаются в summary, пока пользователь +не разместит соответствующий HA device существующим workflow. -Cross-space link не рисуется как линия через разные планы; endpoints получают -badge/count и список другого пространства. Hidden/removed/disabled marker не -рисуется. Rebind refreshes mapping без нового network scan. +## 6. Связи между пространствами и этажами -## Performance и проверки +### 6.1 Почему нельзя рисовать одну линию -- 20/100/500 nodes, dense mesh; edge cap/viewport culling после Stage 0; -- provider fixtures, timeout, malformed/cyclic/duplicate links; -- mapping IEEE/device/entity, cross-space, hidden lifecycle; -- lazy request/in-flight cache and no fetch on ordinary HA ticks; -- golden LQI/unknown legend и performance budget; -- network layer полностью исчезает при toggle off и не меняет config. +Каждое пространство House Plan имеет собственные viewBox, камеру, масштаб и +проекцию. Координаты `(x, y)` первого этажа не имеют геометрического отношения к +таким же координатам второго. Линия до координаты remote marker на текущем SVG +создала бы вымышленное направление и длину. + +Кроме того, порядок вкладок не является физической высотой: пространство может +быть двором, гаражом или отдельным строением. Поэтому стрелки «вверх/вниз» по +порядку вкладок запрещены. + +### 6.2 Контракт cross-space edge + +Если оба drawable endpoint находятся в текущем пространстве, рисуется обычная +линия. + +Если локальный endpoint находится в текущем пространстве, а второй — в другом: + +- линия на плане не рисуется; +- возле локального endpoint появляется компактный topology port/badge с общим + количеством cross-space links; +- click/tap открывает panel, сгруппированный по **названию пространства**; +- строка показывает remote device, provider, directional LQI и роль, не выдавая + наблюдение за гарантированный текущий маршрут; +- действие «Показать» переключает полную карточку на destination space, + центрирует и кратко подсвечивает remote marker; topology layer остаётся + включённым и использует тот же snapshot; +- после перехода прежний endpoint становится remote и получает симметричное + представление. + +Topology port — UI overlay, а не часть plan geometry: он не меняет bounding box, +fit/zoom, layout и export. На touch это полноценный tap target не менее 44 CSS px; +hover не является единственным способом открыть список. + +### 6.3 Пограничные случаи + +- Remote marker hidden/removed/HA-disabled/unmatched: link попадает в summary + «не показано» с причиной, но не в cross-space badge. +- Fixed `floor` full card: список виден, но переход в недоступное пространство + отсутствует. Нельзя обходить fixed-floor контракт. +- `houseplan-space-card`: topology не показывается — карточка статична и + pointerless по действующему контракту. +- Kiosk: topology toggle и диагностическая panel не экспонируются; случайно + сохранённое UI-состояние не включает слой в kiosk. +- Несколько House Plan cards используют shared snapshot, но не рисуют линии + между DOM экземплярами. +- Изометрическая проекция использует уже спроецированную локальную координату + marker; cross-space port остаётся screen overlay и не получает fake Z. + +## 7. Fetch, кеш и permissions + +- Feature доступна только HA admin. Нельзя проксировать ZHA/MQTT через House Plan + backend так, чтобы расширить штатные права non-admin. +- Overlay выключен по умолчанию и не сохраняется в plan config/localStorage. +- Показ уже полученного snapshot не вызывает network scan. +- «Показать слой» и «Пересканировать сеть» — разные действия. Z2M scan запускает + только явная кнопка с предупреждением «10 секунд — 2 минуты; сеть может отвечать + медленнее». +- Shared cache: per HA connection + provider + instance, memory-only, in-flight + dedupe. Snapshot живёт до reload/remount connection; после 5 минут помечается + stale, но не обновляется автоматически. +- Toggle off прекращает progress UI, но не пытается отменить уже начатый radio + scan. Adapter сохраняет успешный ответ в memory cache и гарантированно снимает + MQTT subscription. +- Z2M timeout — 150 секунд после publish; transaction обязателен. Late response + игнорируется. Retained response до request с другим transaction игнорируется. +- Ни HA state tick, ни space switch, ни hover, ни remount второй карточки не + запускают scan. +- IEEE и raw topology не пишутся в plan config, diagnostics по умолчанию или + console. Из provider-данных в config допустим только явно заданный + `baseTopic`, потому что это connection setting, а не topology snapshot. + +## 8. Визуальная семантика + +- Edges — отдельный pointer-transparent lazy layer под devices и над планом. +- Толщина/opacity показывают LQI. Цветовая шкала должна переиспользовать + канонический House Plan LQI resolver, но не усреднять две стороны. +- При двух directional observations tooltip/list показывает обе: `A→B` и + `B→A`. При одной — только подтверждённую сторону. +- Arrow допускается только для подтверждённой directional observation. Направление + Zigbee никогда не смешивается с направлением «на другой этаж». +- Unknown LQI — нейтральный пунктир. Provider failures — warning в panel, не + красная линия «плохой связи». +- Стены не окрашиваются и не объявляются причиной слабого сигнала: overlay + показывает корреляцию в пространстве, не RF-симуляцию. + +## 9. Ограничения достоверности + +- Neighbor tables — наблюдаемый snapshot, а не лог всех текущих packet routes. +- Спящие end devices могут отсутствовать или иметь устаревшую связь. +- Pair может быть видна только с одной стороны; это нормальный результат. +- Coordinator/router failures могут оставить частичный связный граф. +- LQI 0..255 сопоставим по диапазону, но два provider не гарантируют одинаковую + методику и время измерения. Межпровайдерные рейтинги качества не строятся. +- Нельзя выводить причинный текст «стена ухудшает сигнал». + +## 10. Рекомендуемая последовательность реализации + +### Этап A — provider-neutral foundation + +- normalized model, validation/limits, exact registry matching; +- same-space edge projection и cross-space panel/navigation; +- обезличенные ZHA/Z2M fixtures; +- memory cache, capability/error states, lazy bundle boundary. + +### Этап B — ZHA cached adapter + +- admin-only `zha/devices`; +- cached topology без принудительного scan; +- явная маркировка provider-cache и отсутствия точного capture timestamp. + +### Этап C — Zigbee2MQTT adapter + +- настройки одной/нескольких provider instances (`baseTopic`); +- проверка `bridge/info`; +- HA MQTT subscribe + publish, raw networkmap с `routes:false`, transaction, + timeout/cleanup. + +### Этап D — ZHA on-demand scan отдельно + +Только после одного из условий: + +1. upstream `zha/topology/update` даёт completion-aware result/event; или +2. отдельный compatibility adapter доказан на поддерживаемой HA matrix и имеет + fail-closed capability probe. + +Этап D не блокирует полезный cached ZHA overlay и не должен задерживать Z2M. + +## 11. Проверки будущей реализации + +- unit: ZHA/Z2M normalization, directional pair merge, invalid/self/duplicate, + payload limits, provider failures; +- unit: IEEE → HA registry → exact marker, coordinator special identifier, + entity-owner fallback, ambiguous/hidden/removed/disabled; +- unit: same-space/cross-space/unplaced classification, fixed floor, no tab-order + up/down semantics; +- backend или adapter contract: admin-only, unsupported provider, MQTT absent, + wrong base topic, transaction mismatch, retained/late response, timeout, + subscription cleanup; +- smoke desktop + touch View: toggle, progress, cross-space list, transition and + focus remote marker; topology controls absent in kiosk/static card; +- golden: same-space lines, one-way/two-way/unknown, cross-space badge/panel, + partial graph warning; +- performance: 20/100/500 nodes, dense malformed graph, edge cap and no work on + ordinary HA state ticks; +- privacy: topology and IEEE do not enter exported config, diagnostics or logs. + +## 12. Риски, откат и non-scope + +Главные риски: нестабильный ZHA refresh contract, дорогой Z2M scan, неправильный +base topic, неоднозначное размещение составного device, перегруженный dense graph +и ложное восприятие snapshot как реального маршрута. + +Откат каждого provider независим: capability отключается, общий план и LQI +продолжают работать. Поскольку snapshot не сохраняется, миграция и cleanup +пользовательских данных не нужны. Настройка Z2M base topic игнорируется при +отключённом adapter и может быть удалена без изменения плана. + +Не входят в #54: изменение Zigbee-сети, автоматическое создание markers, +RF-прогноз по стенам, history, background auto-scan, cross-building 3D-линия, +support non-admin, topology в kiosk и `houseplan-space-card`. + +## 13. Решения, принятые технически и доступные для пересмотра на ревью + +- Default Z2M base topic — `zigbee2mqtt`, но scan невозможен до успешной проверки + instance. +- Provider cache stale после 5 минут; это только label, не auto-refresh trigger. +- Z2M hard timeout 150 секунд. +- `routes:false` в первом Z2M этапе. +- Multiple markers одного HA device считаются ambiguous, если нет единственного + exact device binding. +- Cross-space detail — panel/list с переходом, без межплановой линии и без + «вверх/вниз» по порядку вкладок. + +## 14. Источники Stage 0 + +- Home Assistant Core ZHA WebSocket API: + https://github.com/home-assistant/core/blob/dev/homeassistant/components/zha/websocket_api.py +- Home Assistant Frontend ZHA data contract: + https://github.com/home-assistant/frontend/blob/dev/src/data/zha.ts +- Home Assistant Frontend graph normalization: + https://github.com/home-assistant/frontend/blob/dev/src/panels/config/integrations/integration-panels/zha/zha-network-data.ts +- zigpy topology scanner: + https://github.com/zigpy/zigpy/blob/dev/zigpy/topology.py +- Home Assistant MQTT WebSocket subscription: + https://github.com/home-assistant/core/blob/dev/homeassistant/components/mqtt/__init__.py +- Home Assistant `mqtt.publish` action: + https://www.home-assistant.io/actions/mqtt.publish/ +- Zigbee2MQTT networkmap API: + https://www.zigbee2mqtt.io/guide/usage/mqtt_topics_and_messages.html#zigbee2mqttbridgerequestnetworkmap +- Zigbee2MQTT networkmap implementation/model: + https://github.com/Koenkk/zigbee2mqtt/blob/master/lib/extension/networkMap.ts + and https://github.com/Koenkk/zigbee2mqtt/blob/master/lib/types/api.ts