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

13 KiB
Raw Blame History

aliases, created, namespace, related, tags, title, type, updated
aliases created namespace related tags title type updated
haos local addon
MQTT discovery HA
датчики HA из аддона
HAOS addon sensors
2026-09-17 family
family/tech/t610-hw-metrics-addon
family/how-to/home-automation
family/how-to/ha-automations
family
tech
smarthome
haos
skill
🧩 HAOS: локальный аддон → датчики в HA UI (skill) tech 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 хоста) та же ошибка
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"]
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 и /sysMemTotal и hwmon0 это значения хоста, а не контейнера. Именно это делает возможными метрики хоста. Проверять для каждого аддона (head -2 /proc/meminfo → сравнить с хостом).

⚠️ На HAOS долгоживущий общий путь — /share (sda8, рядом с /config, /backup, /addons). /mnt/data НЕ существует — частое ложное допущение, скопированное с других типов установки HA.


3. Жизненный цикл установки/обновления (порядок обязателен)

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>/infostate: 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.04.0.0).
  2. uninstallstore/reloadinstall.

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:

{"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:
printf '%s,...\n' "$ROW" >> "$LOGF"
sync "$LOGF" 2>/dev/null || sync     # ← без этого строка умрёт в page cache вместе с хостом

Добавлять поле статуса (например RC=0/1), чтобы сбой публикации был виден в самом логе.


8. Чек-лист проверки

  1. GET /addons/local_<slug>/infostate: 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 заданы, чтобы дашборд зоны мог их показать.

Связанные