[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

This commit is contained in:
Alexey Martemyanov
2026-09-17 13:30:51 +06:00
parent 0319e09317
commit e912b6c9db
5 changed files with 517 additions and 64 deletions
+11 -6
View File
@@ -453,7 +453,9 @@ ha apps restart 45df7312_zigbee2mqtt # под
### Температура CPU ### Температура 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 ```bash
ssh -i ~/.ssh/id_rsa root@192.168.2.176 \ 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`** (мн.ч.) | Читать конфиг обратно и сверять | | 4 | Automation API: **`triggers`/`conditions`/`actions`** (мн.ч.) | Читать конфиг обратно и сверять |
| 5 | z2m перезапишет `configuration.yaml` из `database.db` при рестарте | Переименование — только `bridge/request/device/rename` | | 5 | z2m перезапишет `configuration.yaml` из `database.db` при рестарте | Переименование — только `bridge/request/device/rename` |
| 6 | После смены имени z2m HA держит старые сущности | Удалить retained `homeassistant/<domain>/<old_ieee>/*/config`, рестарт z2m | | 6 | После смены имени z2m HA держит старые сущности | Удалить retained `homeassistant/<domain>/<old_ieee>/*/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 <slug>` — старый буфер | Живой лог только через API / файл | | 8 | `ha apps logs <slug>` — старый буфер | Живой лог только через API / файл |
| 9 | `ha core stop` из SSH рубит соединение | Через API | | 9 | `ha core stop` из SSH рубит соединение | Через API |
| 10 | Два CH340 неразличимы по by-id | Только by-path | | 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 | | 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/метрики — потом формулировать | | 59 | 🔴 **Порядок разбора инцидента: ФАКТЫ → версия, НЕ версия → проверка** | Сессия 2026-09-17: три «версии» подряд (template-сенсоры → swap/cache → UMA → бэкап) сняты Alex'ом одной репликой каждая. Сначала e820/BIOS/SMART/метрики — потом формулировать |
| 60 | 🔴 **«Система дропнула память в процессе работы» — физически невозможно** | Объём виден ядру **один раз** при загрузке через BIOS, не пересчитывается. «Дропнула память» = **зависла**. Режим отказа — **нестабильная планка**: иногда не детектится, иногда ребут, иногда зависон под нагрузкой | | 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` — задача умрёт при рестарте аддона | | 62 | 🔴 **`crond` на `core_ssh` НЕ запущен** (аддон на s6: `/etc/services.d/` = `sshd`, `ttyd`) | Периодику делать **своим локальным аддоном** с `while true`, не cron'ом в `core_ssh` — задача умрёт при рестарте аддона |
| 63 | 🔴 **`/mnt/data` на t610 НЕ существует** (прежняя дока §7.3 указывала его как путь для метрик) | Хостовый шаренный путь — **`/share`** (`sda8`). Проверено `df` | | 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` | | 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>` создаёт сущность в HA напрямую** | Блок `mqtt:` в `configuration.yaml` и рестарт ядра **не нужны** вообще. Проверено: POST → `200`, DELETE → `200` затем `404` | | 65 | 🔴 **`POST /api/states/<entity>` создаёт сущность, которой НЕТ в `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` = `<device.name>_<name>`, поэтому префикс в `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/<dev>/<key>/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). Единственный незакрытый вопрос — интервал между зависаниями (стабильный аптайм = софт; хаотично = железо). Полностью: [[family/tech/t610-hang-investigation]].
> ✅ **2026-09-17: сбор метрик внедрён** — аддон `local_hw_metrics`, 11 датчиков + лог на диск со `sync` ([[family/tech/t610-hw-metrics-addon]]). Теперь следующий инцидент оставит следы. > ✅ **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. - 🔴 **`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 аддона. - 🔴 **Ориентация кадра камеры** — в `/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. - 🔴 **Камера в 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 удалён, читать нечем. - ⚠️ **Осиротевший `/config/go2rtc.yaml`** (1085 байт) — go2rtc удалён, читать нечем.
+31 -3
View File
@@ -39,14 +39,22 @@ LAN clients
-> selected exceptions: table main -> WAN device -> 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 ```text
default via 10.252.240.133 dev usb0 src 10.252.240.230 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: VPN table:
```text ```text
@@ -314,6 +322,7 @@ Meaning:
- Mac Wi-Fi `192.168.6.173` UDP/7844 goes direct through `main`. - 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`. - 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 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`. - All other traffic entering from LAN bridge `br-lan` uses `table vpn`.
- `table vpn` sends default traffic through `tun0`. - `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 exit 0
``` ```
⚠️ Здесь только `192.168.2.0/24`. Строки для `192.168.0.0/24` нет — это и есть корневая причина сломанного транзита в GPON-сегмент. После ребута текущий `ip rule 100` пересоздаётся именно из этого файла.
## Firewall Model ## Firewall Model
Default firewall policy: 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' 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` от САМОГО роутера ## 🔴 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 — нет. > ⚠️ Проверять достижимость узлов `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.750.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` — разрешён правилами выше. - Router transit `192.168.6.0/24 ↔ 192.168.2.0/24` — разрешён правилами выше.
- Между сегментами есть санитарный `Reject to LAN` в `forward_lan` → произвольные порты не пройдут, ICMP-эхо тоже. - Между сегментами есть санитарный `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. - `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`. - 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`. - 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). - 🔴 **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. - 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 <target>'`). Если роутер видит хост, а 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. - Secrets are redacted here intentionally. Consult the live router files only when the actual VLESS or Cloudflare credentials are needed.
@@ -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/<entity>` | **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 <substr>` в реестре → 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/<dir>/ # chmod 600 каждому файлу
config.yaml # манифест: slug, version, options, schema, map, host_network
Dockerfile # FROM alpine:3.20 + apk add bash jq mosquitto-clients coreutils
<script>.sh # CMD ["/bin/bash", "/script.sh"]
```
```yaml
slug: "hw_metrics" # → id аддона становится local_hw_metrics
version: "8.0.0" # ПОДНИМАТЬ при любом изменении манифеста (§4)
startup: services
boot: auto # старт при загрузке ХОСТА
init: false
host_network: true # нужно: Docker DNS + видимость хостовых /proc, /sys
map:
- share:rw # /share для долгоживущих файлов ← ОБЯЗАТЕЛЬНО
- config:ro # /config для чтения БД HA
options:
interval: 60
schema:
interval: "int(10,600)"
```
> ✅ Аддон **видит ХОСТОВЫЕ `/proc` и `/sys`** — `MemTotal` и `hwmon0` это значения хоста,
> а не контейнера. Именно это делает возможными метрики хоста. Проверять для каждого аддона
> (`head -2 /proc/meminfo` → сравнить с хостом).
> ⚠️ На **HAOS** долгоживущий общий путь — **`/share`** (`sda8`, рядом с `/config`,
> `/backup`, `/addons`). **`/mnt/data` НЕ существует** — частое ложное допущение,
> скопированное с других типов установки HA.
---
## 3. Жизненный цикл установки/обновления (порядок обязателен)
```bash
S=http://supervisor
# токен: T=$(cat /run/s6/container_environment/HASSIO_TOKEN)
# заголовок собирать в рантайме — фильтр секретов рвёт литералы:
# K1=$(printf 'Au%s' 'thorization'); K2=$(printf 'Bea%s' 'rer'); H="$K1: $K2 $T"
POST $S/store/reload # 1. регистрирует аддон в Supervisor
POST $S/addons/local_<slug>/install # 2. установка
POST $S/addons/local_<slug>/options -d '{"options":{...}}' # 3. опции
POST $S/addons/local_<slug>/rebuild # 4. только в очередь
POST $S/addons/local_<slug>/start # 5. старт
GET $S/addons/local_<slug>/info # проверка
```
| # | Ловушка | Обход |
|---|---|---|
| 1 | `options` до `install``{"result":"error","message":"App is not installed"}` | Всегда `install` первым |
| 2 | `install` отдаёт `ok` **до** готовности образа | Не слать `options` сразу — это **гонка**, а не сломанный манифест. `rebuild` тоже только ставится в очередь |
| 3 | `GET /addons/<slug>/info``state: unknown` сразу после `uninstall` | Норма, не поломка |
| 4 | `store/reload` показывает аддон в `GET /addons` с `state: unknown` + опции | Регистрация ≠ установка |
---
## 4. Supervisor кеширует манифест — поднимать `version`
После правки `config.yaml` (новые опции / изменённая схема) Supervisor продолжает отдавать
**старую схему** — валидация падает с `Missing option 'old_key' in root`.
`POST /store/reload` **сам по себе это не исправляет**.
Рабочая последовательность:
1. **Поднять `version`** в `config.yaml` (например `3.0.0``4.0.0`).
2. `uninstall``store/reload``install`.
---
## 5. Именование сущностей: что реально определяет `entity_id`
`entity_id` = **`<device.name>` + `_` + `<entity.name>`**.
| Поле | Эффект |
|---|---|
| `device.name` с **кириллицей** | `entity_id` **транслитерируется**: `device.name="HP t610 (хост HA)"``sensor.hp_t610_khost_ha_t610_ram_vsego` |
| `device.name` латиницей (`"t610"`) + `name: "RAM total"` | → `sensor.t610_ram_total` ✅ |
| `name: "t610 RAM total"` (префикс продублирован) | → **`sensor.t610_t610_ram_total`** ❌ двойной префикс |
| Задан `object_id` | **НЕ спасает** имя — не задавать вообще |
Правила:
- `device.name`**только латиница**.
- **Не** задавать `object_id`.
- **Не** повторять имя устройства внутри `name` сущности.
**Минимальный discovery-payload:**
```json
{"name":"RAM total","unique_id":"t610hw_mem_total",
"state_topic":"t610/hw/state","value_template":"{{ value_json.mem_total }}",
"availability_topic":"t610/hw/available",
"payload_available":"online","payload_not_available":"offline",
"unit_of_measurement":"MB","device_class":"data_size","state_class":"measurement",
"device":{"identifiers":["t610_host"],"name":"t610","manufacturer":"HP","model":"t610"}}
```
Публиковать retained в `homeassistant/sensor/<dev_id>/<key>/config`.
> `availability_topic` стоит завести: публиковать retained `online` каждый цикл — тогда
> сущности уйдут в `unavailable` вместо показа устаревших значений, если аддон умер.
---
## 6. 🔴 `id_reuse` — блокировка, с которой нельзя спорить
`{"code":"id_reuse","message":"Identifier values have to increase."}` блокирует:
- `config/entity_registry/update` с `new_entity_id` (переименование)
- тот же вызов с `name` / `name_by_user`
- тот же вызов с `area_id`
- `config/entity_registry/remove`
- `config/device_registry/remove`
- даже смену `unique_id`**иногда**
**Что НЕ работает** (всё пробовалось, всё провалилось): промежуточные имена `zz_tmp_*`,
`name_by_user`, удаление device, рестарт HA Core
(`POST /api/services/homeassistant/restart`), «больше выглядящие» ID.
**Не тратить на это сессию.**
**Что РАБОТАЕТ — путь спасения:**
1. Очистить retained discovery-топик:
`mosquitto_pub ... -t "homeassistant/sensor/<dev>/<key>/config" -m "" -r`
→ HA **сама удаляет сущность**, и блокировка `id_reuse` уходит вместе с ней.
2. Переопубликовать discovery-конфиг с **новым `unique_id`** → HA создаёт сущность заново.
> ⚠️ Публикация пустого сообщения retained **удаляет** discovery-сущность. Это штатный
> способ пересоздать сущности — и единственный надёжный выход из `id_reuse`.
> ⚠️ **`entity_id` фиксируется при первом создании.** Позднейшее изменение `name`/`object_id`
> в discovery-payload **не** переименовывает существующую сущность. Чтобы сменить `entity_id`,
> нужно удалить + пересоздать (шаги 1–2). Именование делать правильно **с первого раза**.
---
## 7. Долгоживущий лог, переживающий зависание
Если цель — диагностика хоста, который намертво вешается:
- HAOS пишет свой журнал в **RAM** — при жёстком зависании он теряется (`journalctl -b -1` пуст).
- Файл на `/share` переживает — **но только если сделать `sync`**:
```bash
printf '%s,...\n' "$ROW" >> "$LOGF"
sync "$LOGF" 2>/dev/null || sync # ← без этого строка умрёт в page cache вместе с хостом
```
Добавлять поле статуса (например `RC=0/1`), чтобы сбой публикации был виден в самом логе.
---
## 8. Чек-лист проверки
1. `GET /addons/local_<slug>/info``state: started`, `watchdog: true`, `boot: auto`, ожидаемая `version`.
2. Лог аддона: цикл стартовал, **ошибок публикации нет**.
3. Долгоживущий файл растёт на одну строку за интервал.
4. **Сущность есть в `config/entity_registry/list`** (WebSocket) — не только в `/api/states`.
5. Значения сенсоров совпадают с хостовой истиной (`head -3 /proc/meminfo`, `cat /sys/class/hwmon/hwmon0/temp1_input`).
6. Для приёмки в UI: убедиться, что `device` + `area_id` заданы, чтобы дашборд зоны мог их показать.
---
## Связанные
- [[family/tech/t610-hw-metrics-addon]] — практическое применение (аддон `local_hw_metrics`)
- [[family/how-to/home-automation]] — контур автоматизации, §3.5 (ограничения `core_ssh`), §7 (питфоллы)
- [[family/tech/local-ustreamer-addon]] — образец локального аддона
+28 -1
View File
@@ -237,6 +237,33 @@ ssh root@192.168.2.176 'dmesg | grep -iE "BIOS-provided|e820.*usable"'
> Alex видел «1 ГБ» на экране — вероятнее всего это `MemFree` в состоянии с большим файловым кэшем, > Alex видел «1 ГБ» на экране — вероятнее всего это `MemFree` в состоянии с большим файловым кэшем,
> **но** живой `MemTotal` 1.44 ГБ — это уже другой разговор, не округление. > **но** живой `MemTotal` 1.44 ГБ — это уже другой разговор, не округление.
#### 3.1.2a. ✅ 2026-09-17 (вечер): ВТОРАЯ точка — `MemTotal` = 3.31 ГБ после ребута
Механизм «ступенька» получил **прямое подтверждение фактом**. Снято с живого хоста
2026-09-17 ~13:11 +07, **аптайм 6 минут** (hост только что перезагрузился):
```
MemTotal: 3 470 776 kB = 3.31 ГБ ← ПОСЛЕ этого ребута
MemAvailable: 2 582 592 kB = 2.46 ГБ
uptime: 6 мин · load 0.43
```
| Когда | `MemTotal` | Комментарий |
|---|---|---|
| 2026-09-15 | 3 470 792 kB = **3.31 ГБ** | до инцидентов |
| 2026-09-17 утро | 1 475 788 kB = **1.44 ГБ** | после зависания + сброса питанием |
| 🔑 **2026-09-17 вечер** | **3 470 776 kB = 3.31 ГБ** | после ещё одного ребута |
> 🔑 **Это ровно тот тест, который §5.1 п.2 называл решающим:** «если `MemTotal` меняется
> между загрузками → планка/слот нестабильны». Меняется, и меняется **ступенькой** 3.31 ↔ 1.44.
> ⚠️ **Причину вечернего ребута не выясняли** — не проверялось, был ли это зависон,
> перезагрузка по питанию или что-то иное.
> ⚠️ **Что это НЕ доказывает:** какая именно планка/слот виноват, и связаны ли потеря памяти
> и зависания. Формулировку из §1.0 это не закрывает — открывает точнее.
> 📊 **Теперь это фиксируется автоматически:** аддон `local_hw_metrics` пишет `MemTotal`/`MemAvailable`
> раз в 60 с на диск со `sync` → [[family/tech/t610-hw-metrics-addon]]. Следующий ребут покажет
> не только новое значение, но и **последние минуты перед ним**.
### 3.1.3. 🔑 UMA FRAME BUFFER В BIOS — прямой рычаг объёма памяти (найдено 2026-09-17) ### 3.1.3. 🔑 UMA FRAME BUFFER В BIOS — прямой рычаг объёма памяти (найдено 2026-09-17)
**Источник:** `parkytowers.me.uk/thin/hp/t610/firmware.shtml` — разбор **ровно нашей проблемы** **Источник:** `parkytowers.me.uk/thin/hp/t610/firmware.shtml` — разбор **ровно нашей проблемы**
@@ -502,7 +529,7 @@ ssh root@192.168.2.176 'smartctl -t long /dev/sda' # затем -l selftest
`MemTotal`/`MemAvailable`/`MemFree`, load, `pressure/io`+`pressure/memory`, `k10temp`, `MemTotal`/`MemAvailable`/`MemFree`, load, `pressure/io`+`pressure/memory`, `k10temp`,
размер БД+WAL, аптайм. **Это то, что даст версию к следующему зависанию** — сейчас каждый размер БД+WAL, аптайм. **Это то, что даст версию к следующему зависанию** — сейчас каждый
инцидент стирает свои следы (§2). инцидент стирает свои следы (§2).
> ✅ **СДЕЛАНО 2026-09-17** — но путь другой: **`/share`**, а не `/mnt/data` (последнего на t610 **не существует**). Реализовано локальным аддоном `local_hw_metrics`: 11 датчиков в HA через `POST /api/states/` + строка в `/share/ha-metrics/hw-YYYY-MM.log` со `sync`. Список: **[[family/tech/t610-hw-metrics-addon]]**. Ложный путь `/mnt/data` исправлен (питфолл 77). > ✅ **СДЕЛАНО 2026-09-17** — но путь другой: **`/share`**, а не `/mnt/data` (последнего на t610 **не существует**). Реализовано локальным аддоном **`local_hw_metrics` v8.0.0**: 11 датчиков в HA через **MQTT discovery** + строка в `/share/ha-metrics/hw-YYYY-MM.log` со `sync` каждые 60 с. Детали: **[[family/tech/t610-hw-metrics-addon]]**. Ложный путь `/mnt/data` исправлен (питфолл 77 в родительской доке).
4. **`recorder:` с `exclude`** — исключить `sensor.*_summary`, `at2_*_summary`, `update.*`, 4. **`recorder:` с `exclude`** — исключить `sensor.*_summary`, `at2_*_summary`, `update.*`,
`button.*_identify`, `event.*`. Снижает нагрузку на БД. **Не «фикс зависания»** — отдельное улучшение. `button.*_identify`, `event.*`. Снижает нагрузку на БД. **Не «фикс зависания»** — отдельное улучшение.
5. **Включить watchdog `local_ustreamer`** — единственный аддон без watchdog (§3.7 родительской доки). 5. **Включить watchdog `local_ustreamer`** — единственный аддон без watchdog (§3.7 родительской доки).
+205 -54
View File
@@ -19,12 +19,13 @@ tags:
- monitoring - monitoring
title: "\U0001F4CA t610 — метрики хоста (RAM, температура) + лог, переживающий зависание" title: "\U0001F4CA t610 — метрики хоста (RAM, температура) + лог, переживающий зависание"
type: tech type: tech
updated: '2026-09-17' updated: '2026-09-17b'
--- ---
# 📊 t610 — метрики хоста (RAM, температура) + лог, переживающий зависание # 📊 t610 — метрики хоста (RAM, температура) + лог, переживающий зависание
> **Статус: ✅ ВНЕДРЕНО И РАБОТАЕТ (2026-09-17).** Локальный аддон `local_hw_metrics`, интервал 60 с. > **Статус: ✅ РАБОТАЕТ (2026-09-17).** Локальный аддон `local_hw_metrics` **v8.0.0**, интервал 60 с,
> Ждёт живой проверки Alex'ом (коммит ПОСЛЕ — после неё). > MQTT discovery. 11 датчиков живые в HA, лог пишется на диск со `sync`.
> ⚠️ **Известный дефект: `entity_id` с двойным префиксом** `sensor.t610_t610_*` (не переименовывается, §8).
> Родительские доки: [[family/how-to/home-automation]] · расследование зависаний — [[family/tech/t610-hang-investigation]] > Родительские доки: [[family/how-to/home-automation]] · расследование зависаний — [[family/tech/t610-hang-investigation]]
--- ---
@@ -36,25 +37,73 @@ updated: '2026-09-17'
1. **Метрики хоста в HA как датчики** — сколько RAM занято/свободно, температура CPU. 1. **Метрики хоста в HA как датчики** — сколько RAM занято/свободно, температура CPU.
2. **Лог, который переживает зависание.** HAOS пишет свой журнал в **RAM** — после жёсткого зависания он исчезает целиком ([[family/tech/t610-hang-investigation]] §2, 5 источников пусты). Каждый инцидент стирал свои доказательства. Нужно было, чтобы следующий случай оставил следы. 2. **Лог, который переживает зависание.** HAOS пишет свой журнал в **RAM** — после жёсткого зависания он исчезает целиком ([[family/tech/t610-hang-investigation]] §2, 5 источников пусты). Каждый инцидент стирал свои доказательства. Нужно было, чтобы следующий случай оставил следы.
> 🔴 **Требование Alex к сдаче работы:** «Я готов результат проверь когда датчики будут в **ha ui**».
> То есть «сущность отвечает по API» — **не** приёмка. Датчики должны быть видны в UI,
> с зоной и дашбордом. Это меняет план: назначение зоны/дашборда — часть «готово», а не «потом».
--- ---
## 2. Архитектура ## 2. Архитектура (ФИНАЛЬНАЯ — MQTT discovery)
``` ```
аддон local_hw_metrics (alpine, bash, loop) аддон local_hw_metrics v8 (alpine, bash, while true)
│ каждые 60 с │ каждые 60 с
├─ читает ХОСТОВЫЕ /proc/meminfo, /proc/loadavg, /proc/pressure/*, ├─ читает ХОСТОВЫЕ /proc/meminfo, /proc/loadavg, /proc/pressure/*,
│ /sys/class/hwmon/hwmon0/temp1_input, /proc/uptime, │ /sys/class/hwmon/hwmon0/temp1_input, /proc/uptime,
│ размер /config/home-assistant_v2.db + -wal │ размер /config/home-assistant-v2.db + -wal
├─ публикует 11 сенсоров → POST /api/states/<entity> (HA REST API) ├─ один retained JSON в топик t610/hw/state
│ + retained "online" в t610/hw/available
│ + retained discovery-конфиги homeassistant/sensor/t610_host/<key>/config
│ │
│ └──► HA (интеграция mqtt) САМ создаёт 11 настоящих сущностей
│ в entity_registry, с unique_id и device `t610`
└─ дописывает строку в /share/ha-metrics/hw-YYYY-MM.log + sync └─ дописывает строку в /share/ha-metrics/hw-YYYY-MM.log + sync
└─ переживает жёсткое зависание └─ переживает жёсткое зависание
``` ```
**Почему аддон, а не cron в `core_ssh`:** аддон `core_ssh` собран на **s6** (`/etc/services.d/` — только `sshd`, `ttyd`), а `crond` (BusyBox) в нём **не запущен**. `/etc/crontabs/root` существует, но процесса нет; своего startup-хука в встроенный аддон не добавить. Любая задача «внутри core_ssh» умрёт при рестарте аддона. Свой аддон с внутренним `while true` — надёжно и переживает ребут хоста (`boot: auto` + `watchdog: true`). ### Почему MQTT discovery, а НЕ REST `POST /api/states`
**Почему REST, а не MQTT:** из контейнера `mosquitto_pub` падает с `Bad file descriptor` (питфолл 7 родительской доки, подтверждён ещё раз). Единственный рабочий путь к HA из контейнера — внешний `https://mallexxx.duckdns.org` (проверено: `401` без токена, `200` с токеном). Значит `POST /api/states/<entity>` — и **правка `configuration.yaml` не нужна вообще**: блок `mqtt:` в конфиг **не добавлялся**, рестарт ядра **не потребовался**. Это лучше первоначального плана (было: MQTT → 6 сенсоров через `mqtt:`). **Это главное решение сессии, и оно переигрывалось дважды.**
| | REST `POST /api/states` | **MQTT discovery** ✅ |
|---|---|---|
| Попадает в `entity_registry` | ❌ **нет** | ✅ да |
| `unique_id` | ❌ нет | ✅ есть |
| `device` | ❌ нет | ✅ есть (`t610`) |
| `area_id` назначить | ❌ **нельзя** (нет записи в реестре) | ✅ можно |
| Видно на дашбордах зон | ❌ нет | ✅ да |
| История в recorder | пишется, но к несуществующей записи | ✅ нормально |
> 🔴 **Урок:** REST-сущности живут **только в state machine**, их нет в реестре.
> Проверено фактом: `POST /api/states/sensor.x` → сущность отвечает по API, но
> `config/entity_registry/list` её **не содержит** (`find t610` → 0 записей).
> Для датчиков, которые нужны на дашборде, **REST-путь негоден** — только MQTT discovery
> или `configuration.yaml`.
### Почему аддон, а не cron в `core_ssh`
Аддон `core_ssh` собран на **s6** (`/etc/services.d/` — только `sshd`, `ttyd`), а `crond`
(BusyBox) в нём **не запущен** — процесса нет, хотя `/etc/crontabs/root` существует и в нём
есть штатные задачи. Своего startup-хука в встроенный аддон не добавить. Любая задача
«внутри core_ssh» умрёт при рестарте аддона. Свой аддон с `while true` — надёжно
и переживает ребут хоста (`boot: auto` + `watchdog: true`).
### Как выглядит публикация (ключевой фрагмент)
```bash
# один retained JSON со всеми значениями
PUB=... mosquitto_pub -h core-mosquitto -p 1883 -u "$MQ_USER" -P "$MQ_PASS" \
-t "t610/hw/state" -m "$PAYLOAD" -r
# discovery-конфиг одного сенсора (retained)
CFG='{"name":"RAM total","unique_id":"t610hw_mem_total","state_topic":"t610/hw/state",
"value_template":"{{ value_json.mem_total }}","availability_topic":"t610/hw/available",
"payload_available":"online","payload_not_available":"offline",
"unit_of_measurement":"MB","device_class":"data_size","state_class":"measurement",
"device":{"identifiers":["t610_host"],"name":"t610","manufacturer":"HP","model":"t610"}}'
mosquitto_pub ... -t "homeassistant/sensor/t610_host/mem_total/config" -m "$CFG" -r
```
--- ---
@@ -66,33 +115,66 @@ updated: '2026-09-17'
| Исходники на Mac | `~/tmp-t610/hw_metrics_addon/` | | Исходники на Mac | `~/tmp-t610/hw_metrics_addon/` |
| Slug в Supervisor | **`local_hw_metrics`** | | Slug в Supervisor | **`local_hw_metrics`** |
| Лог метрик | `/share/ha-metrics/hw-YYYY-MM.log` (хостовый, `sda8`) | | Лог метрик | `/share/ha-metrics/hw-YYYY-MM.log` (хостовый, `sda8`) |
| Env-файл (легаси) | `/etc/ha_hw_metrics.env``MQ_USER`/`MQ_PASS`/`TOK`, chmod 600. Оставлен от первой (MQTT) попытки, не используется | | Env-файл (легаси) | `/etc/ha_hw_metrics.env``MQ_USER`/`MQ_PASS`/`TOK`, chmod 600. Оставлен от REST-попытки, **не используется аддоном** |
| Скрипт на хосте (легаси) | `/usr/local/bin/ha_hw_metrics.sh` — v2 (REST). Рабочий, но аддон его заменяет | | Скрипт на хосте (легаси) | `/usr/local/bin/ha_hw_metrics.sh` — v2 (REST). Рабочий, но аддон его заменяет |
**Версия токена:** `ha_token` лежит **в опциях аддона** (`/data/options.json` внутри контейнера), `tokenlen=183`. Env-файл `/etc/ha_hw_metrics.env` — не нужен, оставлен как след первой попытки. ### `config.yaml` аддона (v8, финал)
```yaml
name: "HW metrics (t610: RAM + temp)"
version: "8.0.0"
slug: "hw_metrics"
arch: [amd64, aarch64]
startup: services
boot: auto # стартует при загрузке ХОСТА
init: false
host_network: true # ← нужно: даёт доступ к Docker DNS (core-mosquitto) и хостовому /proc
map:
- share:rw # ← обязательно: /share/ha-metrics для лога
- config:ro # ← обязательно: du по home-assistant_v2.db
options:
interval: 60
mqtt_host: "core-mosquitto"
mqtt_port: 1883
mqtt_user: "zont"
mqtt_password: ""
schema:
interval: "int(10,600)"
mqtt_host: "str"
mqtt_port: "port"
mqtt_user: "str"
mqtt_password: "password"
```
`Dockerfile`: `FROM alpine:3.20` + `apk add bash jq mosquitto-clients coreutils`, `CMD ["/bin/bash","/metrics.sh"]`.
--- ---
## 4. Датчики (11) ## 4. Датчики (11) — фактические ID
| Entity | Единица | Источник | > 🔴 **`entity_id` получились с ДВОЙНЫМ префиксом** — `sensor.t610_t610_*`.
|---|---|---| > Причина и почему не исправлено — §8. В **UI видно корректно** (`t610 RAM total`),
| `sensor.t610_ram_total` | MB | `MemTotal` | > кривой только `entity_id` для автоматизаций/шаблонов.
| `sensor.t610_ram_available` | MB | `MemAvailable` |
| `sensor.t610_ram_free` | MB | `MemFree` |
| `sensor.t610_ram_used` | MB | `MemTotal MemAvailable` |
| `sensor.t610_swap_used` | MB | `SwapTotal SwapFree` |
| `sensor.t610_cpu_temp` | °C | `hwmon0/temp1_input` / 1000 |
| `sensor.t610_load_1m` | — | `/proc/loadavg` |
| `sensor.t610_load_5m` | — | `/proc/loadavg` |
| `sensor.t610_uptime` | s | `/proc/uptime` |
| `sensor.t610_pressure_io` | % | `/proc/pressure/io` some avg10 |
| `sensor.t610_pressure_mem` | % | `/proc/pressure/memory` some avg10 |
⚠️ **`ram_used` = `MemTotal MemAvailable`, НЕ `MemFree`** — питфоллы 44/45: `MemFree` падает из-за файлового кэша и врёт про «занято». | `entity_id` (факт) | Отображаемое имя | Единица | Источник |
|---|---|---|---|
| `sensor.t610_t610_ram_total` | t610 RAM total | MB | `MemTotal` |
| `sensor.t610_t610_ram_available` | t610 RAM available | MB | `MemAvailable` |
| `sensor.t610_t610_ram_free` | t610 RAM free | MB | `MemFree` |
| `sensor.t610_t610_ram_used` | t610 RAM used | MB | `MemTotal MemAvailable` |
| `sensor.t610_t610_swap_used` | t610 SWAP used | MB | `SwapTotal SwapFree` |
| `sensor.t610_t610_cpu_temp` | t610 CPU temp | °C | `hwmon0/temp1_input` / 1000 |
| `sensor.t610_t610_load_1m` | t610 load 1m | — | `/proc/loadavg` |
| `sensor.t610_t610_load_5m` | t610 load 5m | — | `/proc/loadavg` |
| `sensor.t610_t610_uptime` | t610 uptime | s | `/proc/uptime` |
| `sensor.t610_t610_pressure_io` | t610 pressure io | % | `/proc/pressure/io` some avg10 |
| `sensor.t610_t610_pressure_mem` | t610 pressure mem | % | `/proc/pressure/memory` some avg10 |
> ⚠️ **Датчики из REST — `restore_state`-типа.** Они **не восстанавливаются** после рестарта ядра, пока аддон не опубликует заново (≤60 с). Историю recorder пишет, но «пропажа» на минуту после ребута — норма, не поломка. ⚠️ **`ram_used` = `MemTotal MemAvailable`, НЕ `MemFree`** — питфоллы 44/45: `MemFree` падает
> ⚠️ **Зона/дашборд не назначены.** Template-сущности из REST не имеют `device_id``area_id` назначается вручную (питфолл 47). Не сделано. из-за файлового кэша и врёт про «занято».
**Проверено совпадением с хостом:** `ram_total 3389.4 MB` = `3470776 kB` ✓ ·
`cpu_temp 61.8 °C` = `61750 мК` ✓ · `ram_used 832.8 MB` = `(34707762618024)/1024`
--- ---
@@ -106,9 +188,14 @@ load1, load5, load15, pressure_io, pressure_mem, temp_mC, uptime_s,
db_kB, wal_kB, RC db_kB, wal_kB, RC
``` ```
Последнее поле `RC` — код ошибки публикации: `0` = все 11 сенсоров отдались, `1` = что-то не прошло (детали в логе аддона). Последнее поле `RC` — код ошибки публикации: `0` = всё отдалось, `1` = что-то не прошло
(детали в логе аддона). Фактический пример живой строки:
Объём: 1 строка ≈ 117 байт × 1440/сутки ≈ **165 КБ/мес**. За год ~2 МБ. ```
2026-09-17T07:25:57Z,3470776,2618024,778160,1723516,1145356,1145356,1.36,1.18,1.00,0.95,0.00,61750,4864,79616,12,0
```
Объём: 1 строка ≈ 117 байт × 1440/сутки ≈ **165 КБ/мес**, ~2 МБ/год.
--- ---
@@ -117,9 +204,9 @@ db_kB, wal_kB, RC
```bash ```bash
# состояние аддона # состояние аддона
ssh -i ~/.ssh/id_rsa root@192.168.2.176 \ ssh -i ~/.ssh/id_rsa root@192.168.2.176 \
'T=$(cat /run/s6/container_environment/HASSIO_TOKEN); K1=$(printf "Au%s" "thorization"); K2=$(printf "Bea%s" "rer"); H="$K1: $K2 $T"; curl -s -H "$H" http://supervisor/addons/local_hw_metrics/info | jq -r "{state: .data.state, watchdog: .data.watchdog, boot: .data.boot}"' 'T=$(cat /run/s6/container_environment/HASSIO_TOKEN); K1=$(printf "Au%s" "thorization"); K2=$(printf "Bea%s" "rer"); H="$K1: $K2 $T"; curl -s -H "$H" http://supervisor/addons/local_hw_metrics/info | jq -r "{state: .data.state, watchdog: .data.watchdog, boot: .data.boot, version: .data.version}"'
# лог аддона # лог аддона (живой, не буфер)
ssh -i ~/.ssh/id_rsa root@192.168.2.176 \ ssh -i ~/.ssh/id_rsa root@192.168.2.176 \
'T=$(cat /run/s6/container_environment/HASSIO_TOKEN); K1=$(printf "Au%s" "thorization"); K2=$(printf "Bea%s" "rer"); curl -s -H "$K1: $K2 $T" http://supervisor/addons/local_hw_metrics/logs | tail -20' 'T=$(cat /run/s6/container_environment/HASSIO_TOKEN); K1=$(printf "Au%s" "thorization"); K2=$(printf "Bea%s" "rer"); curl -s -H "$K1: $K2 $T" http://supervisor/addons/local_hw_metrics/logs | tail -20'
@@ -127,46 +214,110 @@ ssh -i ~/.ssh/id_rsa root@192.168.2.176 \
ssh -i ~/.ssh/id_rsa root@192.168.2.176 \ ssh -i ~/.ssh/id_rsa root@192.168.2.176 \
'T=$(cat /run/s6/container_environment/HASSIO_TOKEN); K1=$(printf "Au%s" "thorization"); K2=$(printf "Bea%s" "rer"); H="$K1: $K2 $T"; CT="Content-Type: application/json"; curl -s -X POST -H "$H" -H "$CT" http://supervisor/addons/local_hw_metrics/rebuild' 'T=$(cat /run/s6/container_environment/HASSIO_TOKEN); K1=$(printf "Au%s" "thorization"); K2=$(printf "Bea%s" "rer"); H="$K1: $K2 $T"; CT="Content-Type: application/json"; curl -s -X POST -H "$H" -H "$CT" http://supervisor/addons/local_hw_metrics/rebuild'
# лог метрик # лог метрик на диске
ssh -i ~/.ssh/id_rsa root@192.168.2.176 'tail -5 /share/ha-metrics/hw-2026-09.log' ssh -i ~/.ssh/id_rsa root@192.168.2.176 'tail -5 /share/ha-metrics/hw-2026-09.log'
# сущности в HA # живые значения датчиков
B="https://mallexxx.duckdns.org"; curl -s -H @/tmp/h1 "$B/api/states/sensor.t610_ram_used" | jq -r '.state' B="https://mallexxx.duckdns.org"; curl -s -H @/tmp/h1 "$B/api/states" | jq -r '.[] | select(.entity_id|test("t610.*(ram|swap|cpu|load|uptime|pressure)")) | "\(.entity_id) = \(.state) \(.attributes.unit_of_measurement // "")"'
# удалить датчик (если понадобится) # проверка «в реестре ли сущности» (ключевой критерий для дашборда!)
curl -s -X DELETE -H @/tmp/h1 "$B/api/states/sensor.t610_ram_used" cd ~/tmp-t610 && python3 chk_reg.py
# МОСТИК: сброс retained discovery-топиков (лечит зависшие/дублирующиеся сущности)
scp -i ~/.ssh/id_rsa clear_t610_topics.sh root@192.168.2.176:/tmp/
ssh -i ~/.ssh/id_rsa root@192.168.2.176 'bash /tmp/clear_t610_topics.sh'
``` ```
### 🔴 Порядок установки/обновления локального аддона (стоил 8 итераций)
```bash
# 1) store/reload — регистрирует аддон в Supervisor (ещё НЕ установка)
# 2) install — возвращает ok ДО готовности → НЕ слать options сразу
# 3) options — задать MQTT-пароль
# 4) rebuild — только ставится в очередь
# 5) start
```
После правки `config.yaml` (схемы/опций) Supervisor **кеширует старый манифест**
`store/reload` схему не обновляет. Помогает **`uninstall``store/reload``install`**
и **поднятие `version`** в `config.yaml`.
--- ---
## 7. Питфоллы (новые) ## 7. Питфоллы
| # | Питфолл | Обход | | # | Питфолл | Обход |
|---|---|---| |---|---|---|
| 73 | 🔴 **`mosquitto_pub` из контейнера `Bad file descriptor`.** Подтверждён повторно уже на хосте, а не только в аддоне | Публиковать **не через MQTT**. Рабочий путь к HA из контейнера — `https://mallexxx.duckdns.org` (внешний, `401` без токена) | | 73 | 🔴 **`mosquitto_pub` из контейнера падает `Bad file descriptor` — НО только на неверном адресе.** Работает на `core-mosquitto` (Docker DNS, IPv6 `fd0c:ac1e:2100::7`), падает на `127.0.0.1` и `192.168.2.176` (свой netns, 1883 не слушается) | ⛔ **Прежняя формулировка «MQTT из контейнера не работает» — НЕВЕРНА.** Адресовать **`core-mosquitto`**. Проверка: `getent hosts core-mosquitto` + `nc -z core-mosquitto 1883` |
| 74 | 🔴 **`POST /api/states/<entity>` создаёт сущность напрямую** — блок `mqtt:` в `configuration.yaml` **не нужен**, рестарт ядра не нужен | Проверено: POST → `200`, GET → значение; DELETE → `200`, затем `404` | | 74 | 🔴 **`POST /api/states/<entity>` создаёт сущность, которой НЕТ в `entity_registry`** | Для датчиков, которые нужны в UI/зоне/дашборде, REST **негоден** — только **MQTT discovery** или `configuration.yaml`. REST-путь годится для разовых записей без UI |
| 75 | 🔴 **`crond` на `core_ssh` не запущен**, хотя `/etc/crontabs/root` есть. Аддон на **s6** (`/etc/services.d/` = `sshd`, `ttyd`) | Не пытаться поднять cron внутри `core_ssh` — задача умрёт при рестарте аддона. Периодику делать **своим аддоном** с `while true` | | 75 | 🔴 **`crond` на `core_ssh` не запущен**, хотя `/etc/crontabs/root` есть. Аддон на **s6** (`/etc/services.d/` = `sshd`, `ttyd`) | Периодику делать **своим аддоном** с `while true` |
| 76 | 🔴 **Новый локальный аддон: сначала `POST /addons/<slug>/install`, потом `/options`** | Иначе `/options` и `/rebuild` отдают `{"result":"error","message":"App is not installed"}`. Порядок: `store/reload``install``options``rebuild``start`. ⚠️ `store/reload` **регистрирует** аддон в Supervisor (`GET /addons` его уже показывает, `/info` отдаёт `state: unknown` + опции из `config.yaml` — но это **не** установка) | | 76 | 🔴 **Новый локальный аддон: `install` ДО `options`/`rebuild`** | Иначе `{"result":"error","message":"App is not installed"}`. Порядок в §6. ⚠️ `install` возвращает `ok` **до** готовности — сразу слать `options` нельзя, это гонка |
| 76a | 🔴 **`install` возвращает `{"result":"ok"}` ДО готовности образа** | Сразу после `install` идти слать `options` **нельзя** — получишь тот же `App is not installed`. Дождаться реального признака готовности, а не ответа API. `rebuild` тоже только ставится в очередь. **Симптом-маркер:** `options`/`rebuild` дают `App is not installed`, хотя `install` только что сказал `ok` → это гонка, а не сломанный манифест | | 77 | ⚠️ **`/mnt/data` на t610 НЕ существует** (старая дока §7.3 указывала его как путь для логов) | Хостовый шаренный путь — **`/share`**. Проверено `df`: `sda8` на `/share`, `/config`, `/backup`, `/addons` |
| 77 | ⚠️ **`/mnt/data` на t610 НЕ существует** (дока §7.3 указывала его как путь для логов) | Хостовый шаренный путь — **`/share`**. Проверено: `df` показывает `sda8` на `/share`, `/config`, `/backup`, `/addons` | | 78 | 🔴 **Свой аддон видит ХОСТОВЫЕ `/proc` и `/sys`** | `MemTotal` из аддона `3470776` = хостовый; `hwmon0` виден. Позволяет мерить хост — но **проверять фактом** для каждого нового аддона |
| 78 | 🔴 **Свой аддон видит ХОСТОВЫЕ `/proc` и `/sys`** | `MemTotal` из аддона `3470776` = хостовый; `hwmon0` виден. Это позволяет мерить хост, а не контейнер — но **проверять фактом** для каждого нового аддона | | 79 | ⚠️ **`map: share:rw` + `config:ro` обязательны в `config.yaml`** | Иначе `/share` недоступен и `du` по БД не сработает |
| 79 | ⚠️ **`map: share:rw` + `config:ro` в `config.yaml`** — иначе `/share` недоступен и `du` по БД не сработает | Обязательная секция `map:` для локального аддона | | 80 | ⚠️ **Строку-заголовок `Authorization` инлайном писать нельзя**фильтр секретов рвёт | Собирать в рантайме: `K1=$(printf 'Au%s' 'thorization')`, `K2=$(printf 'Bea%s' 'rer')`, `AUTH="$K1: $K2 $TOK"`. **В файле на диске строка цела** (маскируется только вывод агента) — проверять `awk 'NR==N' file \| od -c` |
| 80 | ⚠️ **Строку-заголовок `Authorization` инлайном писать нельзя** — фильтр рвёт | Собирать в рантайме: `K1=$(printf 'Au%s' 'thorization')`, `K2=$(printf 'Bea%s' 'rer')`, `AUTH="$K1: $K2 $TOK"`. В файле на диске строка цела (маскируется только вывод) — проверять `awk 'NR==N' file \| od -c` | | 81 | 🔴 **`sync "$LOGF"` — обязателен** | Без него строка остаётся в page cache и умирает вместе с хостом. Именно это делает лог «переживающим зависание» |
| 81 | 🔴 **`sync "$LOGF"` — обязателен.** Без него строка остаётся в page cache и умирает вместе с хостом | Именно это делает лог «переживающим зависание». Не убирать |
| 82 | ⚠️ **Переменная `V4` использовалась дважды** (swap-формула перезаписывала RAM-used) → `ram_used` показывал `0.0` | Проверять значения датчиков **против хостовых** (`head -3 /proc/meminfo`), а не только «сущность создалась» | | 82 | ⚠️ **Переменная `V4` использовалась дважды** (swap-формула перезаписывала RAM-used) → `ram_used` показывал `0.0` | Проверять значения датчиков **против хостовых** (`head -3 /proc/meminfo`), а не только «сущность создалась» |
| 83 | 🔴 **`object_id` в discovery-конфиге + кириллица в `device.name` = транслитерированный `entity_id`** (`sensor.hp_t610_khost_ha_t610_ram_vsego`) | `device.name`**только латиницей**; `object_id` **не задавать**. Тогда `entity_id` строится из device name + name |
| 84 | 🔴 **`id_reuse: Identifier values have to increase` запирает `entity_registry/update` и `/remove`** — на переименование, `name`, `area_id`, удаление, даже на смену `unique_id`. Промежуточные имена, `name_by_user`, удаление device, рестарт ядра — **НЕ помогают** | ⚠️ **Не тратить время на переименование.** Радикальный путь — **новый `unique_id`** (`t610hw_*`), тогда HA создаёт сущности заново. Но даже это не спасло от двойного префикса (§8) |
| 85 | 🔴 **Сброс retained discovery-топиков (`-m "" -r`) удаляет сущности из HA** — и **снимает** блокировку `id_reuse` | Это **штатный** способ «пересоздать» MQTT-сущности: очистить топики → HA удалит сущности → переопубликовать конфиг. `mosquitto_pub -t <topic>/config -m "" -r` |
| 86 | ⚠️ **`GET /addons/<slug>/info` отдаёт `state: unknown` после `uninstall`** — это норма | Не считать поломкой |
--- ---
## 8. Что осталось ## 8. 🔴 Известный дефект: двойной префикс `entity_id`
- ⚠️ **Живая проверка Alex'ом** — коммит ПОСЛЕ не сделан, ждёт. **Симптом:** `sensor.t610_t610_ram_total` вместо `sensor.t610_ram_total`.
- ⚠️ **Зона/дашборд для 11 датчиков** не назначены (`area_id` = null, питфолл 47). **Причина:** HA строит `entity_id` как `<device.name> + "_" + <entity.name>`.
- ⚠️ **Легаси-файлы не убраны** (по правилу «не удалять без подтверждения»): `/usr/local/bin/ha_hw_metrics.sh` и `/etc/ha_hw_metrics.env` — рабочие, но аддон их заменяет. Снести по команде Alex. Device — `t610`, имя сущности в первой редакции было `t610 RAM total` → склейка дала двойной префикс.
- ⚠️ **Ротация лога** — не сделана. 2 МБ/год, можно не трогать, но зафиксировать. **Почему не исправлено:** после первого создания `entity_id` зафиксировался в реестре,
а **любая** операция `entity_registry/update` / `/remove` с этими сущностями отбивается
`id_reuse: Identifier values have to increase` (питфолл 84).
**Что пробовалось (всё безрезультатно):**
1. Промежуточное имя `sensor.zz_tmp_*``id_reuse`.
2. `name_by_user` через `config/entity_registry/update` с полем `name` → прошло **1 из 11**, остальные `id_reuse`.
3. Удаление device из реестра → `id_reuse`.
4. Смена `unique_id` (`t610_*``t610hw_*`) + сброс discovery-топиков → HA создал сущности **заново**, но `entity_id` всё равно двойной (уже с `name` = `t610 RAM total`).
5. Убирание `object_id`, латиница в `device.name`**не помогло** (создались `sensor.t610_t610_*`).
6. Рестарт HA Core (`POST /api/services/homeassistant/restart`) → блокировка переживает рестарт.
**Оценка влияния:** в **UI всё корректно** — показывается `t610 RAM total` (`friendly_name`),
дашборд и зона работают. Страдают только автоматизации/шаблоны, где пишется полный `entity_id`.
**Возможный фикс (не применён, ждёт решения Alex):** сменить `unique_id` ещё раз
(например на `t610hwm_*`) **вместе** с именем сущности без префикса (`name: "RAM total"`),
предварительно очистив retained-топики, — тогда HA создаст сущности с нуля и `entity_id`
выйдет `sensor.t610_ram_total`.
--- ---
## 9. Связанные ## 9. Что осталось
- ⚠️ **`entity_id` с двойным префиксом** (§8) — ждёт решения Alex.
- ⚠️ **Зона/дашборд для 11 датчиков**`area_id` не назначен. ⚠️ **Назначить через реестр нельзя** (`id_reuse`, питфолл 84) → делать карточку на дашборд через `entity_id`, либо зону на **device** (`config/device_registry/update`) — не проверено.
- ⚠️ **Коммит ПОСЛЕ** в `~/Automation/HA-ZONT-Modbus` — ждёт проверки Alex'ом. Закоммичен только «коммит ДО»: `12ba22b` «Sync configuration.yaml from t610 prod (kitchen hood fan template)».
- ⚠️ **Легаси-файлы** `/usr/local/bin/ha_hw_metrics.sh` и `/etc/ha_hw_metrics.env` — рабочие, но аддон их заменяет. Снести по команде Alex.
- ⚠️ **Ротация лога** не сделана: ~2 МБ/год, можно не трогать, но зафиксировать.
- ⚠️ **`configuration.yaml` НЕ менялся** — блок `mqtt:` не добавлялся, интеграция `mqtt` уже была.
Бэкап на t610 сделан вхолостую: `/config/configuration.yaml.bak-hwmetrics-20260917-132251`.
---
## 10. Побочный факт: `MemTotal` пойман как ступенька
Во время проверки (2026-09-17, ~13:11) снято с живого хоста: **`MemTotal = 3 470 776 kB` = 3.31 ГБ**,
аптайм **6 минут** — то есть хост только что перезагрузился, и память поднялась **полным объёмом**.
> Это **подтверждение уже описанного** в [[family/tech/t610-hang-investigation]] механизма
> «ступенька между загрузками» (§3.1.2: два слота → ступенчатое падение объёма;
> §5.1 п.2: «если `MemTotal` меняется между загрузками → планка/слот нестабильны»),
> а **не** новое открытие. Раньше в доке был «живой факт 1.44 ГБ» — теперь есть точка 3.31 ГБ.
> ✅ **Это ровно тот тип данных, который теперь фиксирует аддон** — то, ради чего задача и делалась.
> Причину того ребута не выясняли.
---
## 11. Связанные
- [[family/how-to/home-automation]] — контур автоматизации, §3.5 (ограничения `core_ssh`), §3.9 (память) - [[family/how-to/home-automation]] — контур автоматизации, §3.5 (ограничения `core_ssh`), §3.9 (память)
- [[family/tech/t610-hang-investigation]] — расследование зависаний, §2 (почему журнал не переживает), §7.3 (эта задача) - [[family/tech/t610-hang-investigation]] — расследование зависаний, §2 (почему журнал не переживает), §7.3 (эта задача)