[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
@@ -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]] — образец локального аддона