From e912b6c9db94af52e13f07a17ff6adf26bc492fa Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 17 Sep 2026 13:30:51 +0600 Subject: [PATCH] [2026-09-17] eagle: family/how-to/home-automation.md family/how-to/rasputin-router.md family/tech/haos-local-addon-publish-sensors.md family/tech/t610-hang-investigation.md family/tech/t610-hw-metrics-addon.md --- family/how-to/home-automation.md | 17 +- family/how-to/rasputin-router.md | 34 ++- .../tech/haos-local-addon-publish-sensors.md | 242 ++++++++++++++++ family/tech/t610-hang-investigation.md | 29 +- family/tech/t610-hw-metrics-addon.md | 259 ++++++++++++++---- 5 files changed, 517 insertions(+), 64 deletions(-) create mode 100644 family/tech/haos-local-addon-publish-sensors.md diff --git a/family/how-to/home-automation.md b/family/how-to/home-automation.md index 661312fb..8fba06e1 100644 --- a/family/how-to/home-automation.md +++ b/family/how-to/home-automation.md @@ -453,7 +453,9 @@ ha apps restart 45df7312_zigbee2mqtt # под ### Температура CPU -> ✅ **2026-09-17: заведено как датчик HA — `sensor.t610_cpu_temp`** через аддон `local_hw_metrics` (вместе с RAM/load/pressure). Разбор — [[family/tech/t610-hw-metrics-addon]]. Ниже — ручной способ через SSH (остаётся рабочим). +> ✅ **2026-09-17: заведено как датчик HA — `sensor.t610_t610_cpu_temp`** (отображаемое имя `t610 CPU temp`) через аддон `local_hw_metrics` (вместе с RAM/load/pressure). +> ⚠️ `entity_id` с **двойным префиксом** `t610_t610_` — известный дефект, не переименовывается ([[family/tech/t610-hw-metrics-addon]] §8). В UI видно корректно. +> Разбор аддона: [[family/tech/t610-hw-metrics-addon]]. Ниже — ручной способ через SSH (остаётся рабочим). ```bash ssh -i ~/.ssh/id_rsa root@192.168.2.176 \ @@ -765,7 +767,7 @@ Hz:PWM 0:0 1:0 2:0 3:0 4:48 5:55 6:63 7:70 8:78 9:85 10:92 11:97 12:102 13:107 1 | 4 | Automation API: **`triggers`/`conditions`/`actions`** (мн.ч.) | Читать конфиг обратно и сверять | | 5 | z2m перезапишет `configuration.yaml` из `database.db` при рестарте | Переименование — только `bridge/request/device/rename` | | 6 | После смены имени z2m HA держит старые сущности | Удалить retained `homeassistant///*/config`, рестарт z2m | -| 7 | `mosquitto_sub`/`pub` в аддоне → `Bad file descriptor` | Публиковать из HA; подписка — с Mac | +| 7 | `mosquitto_sub`/`pub` в контейнере → `Bad file descriptor` — ⚠️ **ТОЛЬКО на неверном адресе** | ⛔ Прежняя формулировка «MQTT из контейнера не работает» **опровергнута 2026-09-17**: работает, если адресовать **`core-mosquitto`** (Docker DNS, IPv6 `fd0c:ac1e:2100::7`). Падает на `127.0.0.1` и `192.168.2.176` — там свой netns, 1883 не слушается. Проверка: `getent hosts core-mosquitto` + `nc -z core-mosquitto 1883`. Разбор — [[family/tech/t610-hw-metrics-addon]] §7 | | 8 | `ha apps logs ` — старый буфер | Живой лог только через API / файл | | 9 | `ha core stop` из SSH рубит соединение | Через API | | 10 | Два CH340 неразличимы по by-id | Только by-path | @@ -818,11 +820,14 @@ Hz:PWM 0:0 1:0 2:0 3:0 4:48 5:55 6:63 7:70 8:78 9:85 10:92 11:97 12:102 13:107 1 | 58 | 🎯 **Симптом-матч с форума ≠ доказанная причина** — тред HA `1025213` (та же версия стека, `sqlite3.OperationalError: disk I/O error`) | Это **сильнейший кандидат**, но закрывать только фактом: `smartctl` + `dmesg`. [[family/tech/t610-hang-investigation]] §5 | | 59 | 🔴 **Порядок разбора инцидента: ФАКТЫ → версия, НЕ версия → проверка** | Сессия 2026-09-17: три «версии» подряд (template-сенсоры → swap/cache → UMA → бэкап) сняты Alex'ом одной репликой каждая. Сначала e820/BIOS/SMART/метрики — потом формулировать | | 60 | 🔴 **«Система дропнула память в процессе работы» — физически невозможно** | Объём виден ядру **один раз** при загрузке через BIOS, не пересчитывается. «Дропнула память» = **зависла**. Режим отказа — **нестабильная планка**: иногда не детектится, иногда ребут, иногда зависон под нагрузкой | -| 61 | 🔴 **`mosquitto_pub` из контейнера — `Bad file descriptor`** (питфолл 7, подтверждён повторно) | Публиковать **не через MQTT**. Рабочий путь к HA из контейнера — внешний `https://mallexxx.duckdns.org`. Разбор — [[family/tech/t610-hw-metrics-addon]] §7 | +| 61 | 🔴 **`mosquitto_pub` в контейнере падает — НО только на неверном адресе** | ⛔ Формулировка «MQTT из контейнера не работает» **опровергнута**: адресовать **`core-mosquitto`** (Docker DNS), а **НЕ** `127.0.0.1`/`192.168.2.176` (свой netns, 1883 не слушается). Разбор — [[family/tech/t610-hw-metrics-addon]] §7 | | 62 | 🔴 **`crond` на `core_ssh` НЕ запущен** (аддон на s6: `/etc/services.d/` = `sshd`, `ttyd`) | Периодику делать **своим локальным аддоном** с `while true`, не cron'ом в `core_ssh` — задача умрёт при рестарте аддона | | 63 | 🔴 **`/mnt/data` на t610 НЕ существует** (прежняя дока §7.3 указывала его как путь для метрик) | Хостовый шаренный путь — **`/share`** (`sda8`). Проверено `df` | -| 64 | ⚠️ **Новый локальный аддон: `install` ДО `options`/`rebuild`** | Иначе `{"result":"error","message":"App is not installed"}`. Порядок: `store/reload` → `install` → `options` → `rebuild` → `start` | -| 65 | 🔴 **`POST /api/states/` создаёт сущность в HA напрямую** | Блок `mqtt:` в `configuration.yaml` и рестарт ядра **не нужны** вообще. Проверено: POST → `200`, DELETE → `200` затем `404` | +| 64 | ⚠️ **Новый локальный аддон: `install` ДО `options`/`rebuild`** | Иначе `{"result":"error","message":"App is not installed"}`. Порядок: `store/reload` → `install` → `options` → `rebuild` → `start`. ⚠️ `install` отдаёт `ok` **до** готовности — сразу слать `options` нельзя (гонка) | +| 65 | 🔴 **`POST /api/states/` создаёт сущность, которой НЕТ в `entity_registry`** | Блок `mqtt:` и рестарт ядра не нужны — но такая сущность **невидима для зон и дашбордов** (нет `unique_id`/`device`/`area_id`). Для UI — только **MQTT discovery** или `configuration.yaml` | +| 66 | 🔴 **MQTT discovery: кириллица в `device.name` + `object_id` → транслитерированный `entity_id`** | `device.name` — только латиницей, `object_id` **не задавать**. `entity_id` = `_`, поэтому префикс в `name` даёт **двойной** (`sensor.t610_t610_*`) | +| 67 | 🔴 **`id_reuse: Identifier values have to increase` запирает `entity_registry/update` и `/remove`** | Воспроизведено на переименовании, `name`, `area_id`, удалении, смене `unique_id`. Промежуточные имена, `name_by_user`, удаление device, рестарт ядра — **не помогают**. **Не тратить время**: единственный рабочий путь — новый `unique_id` + сброс retained-топиков | +| 68 | 🔴 **Сброс retained discovery-топика (`-m "" -r`) удаляет сущность из HA и снимает `id_reuse`** | Штатный способ пересоздать MQTT-сущности: очистить `homeassistant/sensor///config` → HA удалит сущность → переопубликовать конфиг | --- @@ -922,7 +927,7 @@ ssh root@192.168.2.176 'ha apps logs local_modbus-bridge | grep -E "Raw RTU|→ - 🔴 **ЗАВИСАНИЯ ХОСТА — причина не установлена** (2026-09-17). Единственный незакрытый вопрос — интервал между зависаниями (стабильный аптайм = софт; хаотично = железо). Полностью: [[family/tech/t610-hang-investigation]]. > ✅ **2026-09-17: сбор метрик внедрён** — аддон `local_hw_metrics`, 11 датчиков + лог на диск со `sync` ([[family/tech/t610-hw-metrics-addon]]). Теперь следующий инцидент оставит следы. - 🔴 **`recorder:` блока в `configuration.yaml` НЕТ ВООБЩЕ** — HA пишет всё подряд, дефолтные 10 дней, без `exclude`. БД 77 МБ + WAL 4.4 МБ. Кандидат на оптимизацию (НЕ «фикс зависания» — связь не доказана). Введение `exclude` ждёт апрува Alex. -- 🔴 **`MemTotal` живой = 1.44 ГБ**, в §3.9 зафиксировано 3.31 ГБ. Расхождение не объяснено, проверка (`dmesg e820` / `dmidecode`) не запущена. См. §3.9 и [[family/tech/t610-hang-investigation]] §3.1. +- 🔴 **`MemTotal` меняется между загрузками** — 2026-09-17 утром живой замер дал **1.44 ГБ**, вечером (~13:11, аптайм 6 мин после ребута) — **3.31 ГБ**. Это **подтверждение** описанного механизма «ступенька» ([[family/tech/t610-hang-investigation]] §3.1.2, §5.1 п.2), не новое открытие. Причину вечернего ребута не выясняли. См. §3.9 и [[family/tech/t610-hw-metrics-addon]] §10. - 🔴 **Ориентация кадра камеры** — в `/addons/ustreamer/` стоит `transpose=1` (90° по часовой), счётчик лежит на бок. Нужен `transpose=2` + rebuild аддона. - 🔴 **Камера в HA не обновляется** — Generic Camera (`entry_id 01M2FX50K72X2RSYY549QSG3XP`, `camera.192_168_2_176`) смотрит на мёртвые адреса удалённого go2rtc: `still_image_url = http://192.168.2.176:1984/api/frame.jpeg` и `stream_source = rtsp://192.168.2.176:8554/...`. Менять на `http://192.168.2.176:8090/frame`, `stream_source` — пусто. Живёт в `/config/.storage/core.config_entries`, **не в `configuration.yaml`**. См. §4. - ⚠️ **Осиротевший `/config/go2rtc.yaml`** (1085 байт) — go2rtc удалён, читать нечем. diff --git a/family/how-to/rasputin-router.md b/family/how-to/rasputin-router.md index 14c2a7a8..2ef3ad64 100644 --- a/family/how-to/rasputin-router.md +++ b/family/how-to/rasputin-router.md @@ -39,14 +39,22 @@ LAN clients -> selected exceptions: table main -> WAN device ``` -WAN is currently represented by both `eth0` and `usb0` in the `wan` firewall zone. At capture time, `eth0` had no carrier, while `usb0` was up and had the default route. +WAN devices are `eth0` and `usb0` in the `wan` firewall zone. **As of 2026-09-17 `eth0` is the active WAN** (it carries the default route); `usb0` is idle. At the 2026-07-22 capture this was inverted — `eth0` had no carrier and `usb0` held the default route. -Current default route: +Default route at the 2026-07-22 capture (historical, no longer active): ```text default via 10.252.240.133 dev usb0 src 10.252.240.230 ``` +Current default route (2026-09-17): + +```text +default via 192.168.2.2 dev eth0 proto static src 192.168.2.157 +``` + +That is, Rasputin gets `192.168.2.157/24` from the upstream router `192.168.2.2` and reaches the internet through it; `192.168.2.2` in turn sits behind GPON `192.168.0.1`. + VPN table: ```text @@ -314,6 +322,7 @@ Meaning: - Mac Wi-Fi `192.168.6.173` UDP/7844 goes direct through `main`. - Mac Ethernet `192.168.6.104` UDP/7844 goes direct through `main`. - Traffic to upstream LAN `192.168.2.0/24` goes direct through `main`. +- 🔴 Traffic to the GPON segment `192.168.0.0/24` is **NOT** covered by rule `100` (2026-09-17) → falls through to rule `101` → `table vpn` → `tun0`. Доступ к GPON-сегменту из LAN `192.168.6.0/24` не работает. See the «🔴 Транзит Rasputin → 192.168.0.0/24» section. - All other traffic entering from LAN bridge `br-lan` uses `table vpn`. - `table vpn` sends default traffic through `tun0`. @@ -327,6 +336,8 @@ ip rule add to 192.168.2.0/24 lookup main priority 100 exit 0 ``` +⚠️ Здесь только `192.168.2.0/24`. Строки для `192.168.0.0/24` нет — это и есть корневая причина сломанного транзита в GPON-сегмент. После ребута текущий `ip rule 100` пересоздаётся именно из этого файла. + ## Firewall Model Default firewall policy: @@ -421,6 +432,15 @@ firewall.@rule[10].target='ACCEPT' firewall.@rule[10].dest_ip='192.168.2.0/24' '192.168.0.1' ``` +⚠️ **Порядковый номер изменился (2026-09-17).** Актуальное правило — `firewall.@rule[0].name='Allow-upstream-LAN'`, и `dest_ip` расширен до GPON-сегмента. Его nft-реализация в `forward_lan` — **две отдельные строки, `tcp` и `udp`, без `icmp`**: + +```text +meta l4proto tcp ip daddr { 192.168.0.1, 192.168.0.50, 192.168.2.0/24 } counter packets 19 bytes 1216 jump accept_to_wan comment "!fw4: Allow-upstream-LAN" +meta l4proto udp ip daddr { 192.168.0.1, 192.168.0.50, 192.168.2.0/24 } counter packets 0 bytes 0 jump accept_to_wan comment "!fw4: Allow-upstream-LAN" +``` + +Практическое следствие: `forward` в GPON-сегмент разрешён, но без NAT ответ не вернётся, а ICMP вообще не проходит. Полный разбор — в разделе «🔴 Транзит Rasputin → 192.168.0.0/24» ниже. + --- ## 🔴 ICMP к недоступному хосту в 192.168.2.0/24 отдаёт `Destination Port Unreachable` от САМОГО роутера @@ -456,6 +476,10 @@ Vr HL TOS Len ID Flg off TTL Pro cks Src Dst > ⚠️ Проверять достижимость узлов `192.168.2.0/24` из `192.168.6.0/24` надёжнее всего **тем же каналом, которым потом будете работать**: SSH по ключу. `ssh -o ConnectTimeout=10 -o BatchMode=yes root@192.168.2.176 'uptime'` — если отвечает, хост жив, и всё дальнейшее снимается через него. Пинг/веб-порты могут врать (см. `Reject to LAN` ниже), SSH — нет. +> 🔴 **Проверка 2026-09-17: 5 из 5 ICMP-попыток к `192.168.2.2` отбиты роутером** (`Destination Port Unreachable` от `192.168.6.1`), при том что с самого Rasputin `192.168.2.2` пингуется **0% loss, 0.75–0.84 ms**. То есть эхо-запрос из LAN до шлюза не доходит вообще — отбой происходит локально. Практический вывод: **отсутствие пинга к `192.168.2.x` НЕ говорит о том, что хост/шлюз недоступен.** Единственный вывод — «ICMP из LAN до этого адреса зарезан». Никогда не строить на пинге вывод о состоянии аплинка; проверять TCP-каналом, которым будете работать. + +> ⚠️ Ещё один контр-пример к пингу как индикатору (2026-09-17, `192.168.0.50`): с Мака — 100% loss **и без** `Destination Port Unreachable` (обычный timeout), а с Rasputin — 0% loss. Здесь пинг отражает реальную поломку policy routing, но **не** показывает, жив ли хост. Различить «хост мёртв» и «транзит сломан» можно одним дешёвым шагом: пингануть ту же цель **с самого Rasputin**. Роутер видит, клиент — нет ⇒ проблема в транзите, не в хосте. + **Границы прав внутри сегментов (напоминание):** - Router transit `192.168.6.0/24 ↔ 192.168.2.0/24` — разрешён правилами выше. - Между сегментами есть санитарный `Reject to LAN` в `forward_lan` → произвольные порты не пройдут, ICMP-эхо тоже. @@ -767,7 +791,11 @@ uci commit dhcp - `ip rule` can match `ipproto udp dport 7844` on this OpenWrt build; this was tested and confirmed before making the persistent change. - The firewall allow alone would not be sufficient, because policy routing rule `101` sends LAN ingress to `table vpn`. The direct `ip rule`s at priorities `98` and `99` must remain before priority `101`. - Rule order in `forward_lan` matters: Cloudflare direct allow must stay above `KillSwitch`. -- `eth0` was down at the 2026-07-22 capture; `usb0` was active and part of the `wan` firewall zone. +- `eth0` is the **active** WAN as of 2026-09-17 (`192.168.2.157/24`, gw `192.168.2.2`); `usb0` is idle. At the 2026-07-22 capture this was inverted (`eth0` down, `usb0` carrying the default route). Do not trust the old banner — re-read `ip route` before assuming which WAN is live. +- 🔴 **Транзит из LAN `192.168.6.0/24` в GPON-сегмент `192.168.0.0/24` НЕ работает (2026-09-17).** Причин три: `ip rule 100` не покрывает `192.168.0.0/24`; нет NAT/masq для этого направления; ICMP не разрешён в `Allow-upstream-LAN`. План ремонта — в одноимённом разделе. **Пока не применён.** +- 🔴 **ICMP не является индикатором доступности `192.168.0.0/24` — и подавно.** `Allow-upstream-LAN` создан только для `tcp`+`udp`, поэтому эхо-запросы из LAN падают в `KillSwitch` и роутер отвечает `Destination Port Unreachable` от себя. Проверять TCP-портом (`nc -zv 192.168.0.50 80`) или SSH. +- Различай два похожих симптома: `Destination Port Unreachable` **от роутера `192.168.6.1`** = локальное отбивание ICMP/firewall-ом, а не закрытый порт на хосте. Признак живости — ответ на TCP/SSH, а не текст ICMP-ошибки. - 🔴 **Ping from Mac to an unreachable `192.168.2.x` host returns `Destination Port Unreachable` from `rasputin.lan (192.168.6.1)`, NOT a plain timeout.** This is the router rejecting the attempt — it does **not** mean the host is up with a closed port. Use `arp (incomplete)` + `100% packet loss` as the liveness signal. Confirmed 2026-09-15 (see the dedicated section above). - Liveness of `192.168.2.0/24` hosts is best verified with the channel you intend to work through: key-based SSH (`ssh -o BatchMode=yes root@192.168.2.176 'uptime'`). Ping and web ports can mislead; an answered SSH session cannot. +- **Дешёвый способ развести «хост мёртв» и «транзит сломан»:** пингануть цель *с самого Rasputin* (`ssh root@192.168.6.1 'ping -c2 '`). Если роутер видит хост, а LAN-клиент — нет, проблема в policy routing/firewall, а не в хосте. Именно так 2026-09-17 локализовали GPON-проблему. - Secrets are redacted here intentionally. Consult the live router files only when the actual VLESS or Cloudflare credentials are needed. diff --git a/family/tech/haos-local-addon-publish-sensors.md b/family/tech/haos-local-addon-publish-sensors.md new file mode 100644 index 00000000..23694b03 --- /dev/null +++ b/family/tech/haos-local-addon-publish-sensors.md @@ -0,0 +1,242 @@ +--- +aliases: + - haos local addon + - MQTT discovery HA + - датчики HA из аддона + - HAOS addon sensors +created: '2026-09-17' +namespace: family +related: + - '[[family/tech/t610-hw-metrics-addon]]' + - '[[family/how-to/home-automation]]' + - '[[family/how-to/ha-automations]]' +tags: + - family + - tech + - smarthome + - haos + - skill +title: "🧩 HAOS: локальный аддон → датчики в HA UI (skill)" +type: tech +updated: '2026-09-17' +--- +# 🧩 HAOS: локальный аддон → датчики в HA UI + +> **Зеркало скилла** `haos-local-addon-publish-sensors` (категория `devops`). +> Путь: `~/.hermes/hermes-whale/skills/devops/haos-local-addon-publish-sensors/SKILL.md`. +> Дополняет скилл `ha-automation-debugging`. Загружать, когда нужно **завести свои данные +> как сенсоры HA, видимые в UI** (метрики хоста, скрейпинг, всё, что не покрыто интеграцией). +> Практический пример применения — [[family/tech/t610-hw-metrics-addon]]. + +--- + +## 0. Главное решение: MQTT discovery, НЕ REST + +| | `POST /api/states/` | **MQTT discovery** ✅ | +|---|---|---| +| В `entity_registry` | ❌ **нет** | ✅ да | +| `unique_id` / `device` | ❌ / ❌ | ✅ / ✅ | +| Назначить `area_id` | ❌ **невозможно** (нет записи в реестре) | ✅ да | +| Видно на дашбордах зон | ❌ нет | ✅ да | +| История в recorder | пишется, запись осиротевшая | ✅ нормально | + +**REST-сущности живут только в state machine.** Отвечают по `/api/states`, видны в +Developer Tools → States, но **отсутствуют в `config/entity_registry/list`**. +Проверено фактом: `POST /api/states/sensor.x` → 200, затем `find ` в реестре → 0. + +> 🔴 **Проверять по реестру, а не по `/api/states`.** «Отвечает по API» ≠ «сущность настоящая». +> Grep по `config/entity_registry/list` через WebSocket — это и есть приёмка. +> Если пользователь говорит «покажи в UI» — REST уже не подходит. + +**Следствие:** блок `mqtt:` в `configuration.yaml` **не нужен**, если интеграция `mqtt` +уже настроена (обычно так и есть — проверять `/api/config` → `.components` содержит `mqtt`). + +--- + +## 1. MQTT изнутри контейнера — адресовать правильно + +`mosquitto_pub` внутри контейнера аддона **работает** — но только по правильному адресу: + +| Адрес | Результат | +|---|---| +| `core-mosquitto` ✅ | **работает** (Docker DNS; резолвится в IPv6 `fd0c:ac1e:2100::7`) | +| `127.0.0.1` / `localhost` ❌ | `Bad file descriptor` — у аддона свой netns, на 1883 никто не слушает | +| `192.168.2.176` (LAN IP хоста) ❌ | та же ошибка | + +```bash +getent hosts core-mosquitto # DNS резолвится? +nc -z core-mosquitto 1883 && echo OPEN +``` + +> ⚠️ **Неверный питфолл размножается:** ранее в доке было записано «MQTT из контейнера не +> работает вообще». Это был **баг адреса**, а не MQTT. `Bad file descriptor` от +> `mosquitto_pub` = **неверный хост**, а не сломанный клиент. + +--- + +## 2. Структура файлов локального аддона + +``` +/addons// # chmod 600 каждому файлу + config.yaml # манифест: slug, version, options, schema, map, host_network + Dockerfile # FROM alpine:3.20 + apk add bash jq mosquitto-clients coreutils +