243 lines
13 KiB
Markdown
243 lines
13 KiB
Markdown
---
|
||
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]] — образец локального аддона
|