Files
obsidian-vault/family/tech/haos-local-addon-publish-sensors.md
T

243 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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]] — образец локального аддона