310 lines
19 KiB
Markdown
310 lines
19 KiB
Markdown
# ТЗ: Дашборд доступности устройств в Home Assistant
|
||
|
||
## Цель
|
||
Дашборд, показывающий **uptime устройств** в процентах за три периода: день, неделю, месяц. С сортировкой по убыванию доступности.
|
||
|
||
## Метрика
|
||
**Доступность** = время в статусе «доступен» / общее время периода × 100%
|
||
|
||
- «Доступен» — любой статус, кроме `unavailable` и `unknown`. **Важно:** статус `off` (выключенное устройство) считается доступным — выключено ≠ недоступно
|
||
- «Недоступен» — статус `unavailable` или `unknown`
|
||
- Результат: процент (0%–100%), один знак после точки
|
||
|
||
## Периоды
|
||
По умолчанию: **7 дней**. В дашборде — переключатель: 24ч / 7д / 30д.
|
||
|
||
| Период | Обновление | Описание |
|
||
|--------|------------|----------|
|
||
| 24ч | каждые 5 мин | Оперативный мониторинг |
|
||
| 7д | каждые 15 мин | Основной режим по умолчанию |
|
||
| 30д | каждые 2 часа | Долгосрочная статистика |
|
||
|
||
Переключение периода пересчитывает все данные на лету из истории. Храним кэш для всех трёх периодов, чтобы переключение было мгновенным.
|
||
|
||
## Какие устройства отслеживать
|
||
Все entity из доменов физических устройств:
|
||
- `binary_sensor`, `sensor`, `switch`, `light`
|
||
- `climate`, `cover`, `lock`, `media_player`
|
||
- `device_tracker`, `vacuum`, `fan`, `humidifier`
|
||
- `water_heater`, `siren`, `button`
|
||
|
||
**Исключения** (не показывать на дашборде):
|
||
- Вспомогательные entity Zigbee2MQTT (`sensor.*_update_state`, `update.*`, `button.zigbee2mqtt_*`)
|
||
- Entity с суффиксами `_battery_low`, `_battery`, `_linkquality`, `_update`, `_identify`
|
||
- Виртуальные/template сенсоры (без device_id)
|
||
- Автоматически создаваемые Select/Number для Zigbee-устройств (настройки radar_sensitivity и т.п.)
|
||
|
||
## Архитектура решения
|
||
|
||
### 1. Вычисление доступности
|
||
**Один Python-скрипт** (AppDaemon или HA Python custom component):
|
||
1. Периодически (по расписанию для каждого периода) запрашивает историю из HA API
|
||
2. Batch-запрос: несколько entity_id через запятую (`filter_entity_id=id1,id2,id3`)
|
||
3. Считает для каждого устройства:
|
||
- Процент доступности за выбранный период
|
||
- Частоту падений (количество переходов в unavailable/unknown)
|
||
- Максимальный непрерывный даунтайм
|
||
- Sparkline данные (7 точек — по одной за день)
|
||
- Сравнение с предыдущим периодом (тренд)
|
||
4. Записывает результаты как sensor-ы: `sensor.avail_<sanitized_id>` с атрибутами
|
||
|
||
### 2. Структура sensor-ов
|
||
Каждый отслеживаемый device → один sensor с атрибутами:
|
||
|
||
```yaml
|
||
sensor.avail_light_bra_v_spalne:
|
||
state: 97.8 # текущий % доступности
|
||
attributes:
|
||
entity_id: light.bra_v_spalne
|
||
friendly_name: "Бра в спальне"
|
||
domain: light
|
||
period: 7d # выбранный период
|
||
availability_pct: 97.8
|
||
down_count: 5
|
||
max_downtime_minutes: 102
|
||
sparkline: [100, 100, 98, 95, 99, 100, 97.8] # 7 точек
|
||
trend: "down" # down/up/stable vs прошлая неделя
|
||
last_downtime: "2026-04-14T15:32:00+03:00"
|
||
color: "green" # green/yellow/orange/red
|
||
```
|
||
|
||
### 3. Варианты реализации
|
||
|
||
#### Вариант A: AppDaemon (рекомендуемый)
|
||
- Отдельный Python-скрипт в AppDaemon
|
||
- Работает по расписанию, batch-обработка
|
||
- Легко читать/обновлять, не зависит от HA Core
|
||
- ✅ **Плюсы:** изоляция, легко отлаживать, минимум нагрузки на HA
|
||
- ⚠️ **Требует:** установка AppDaemon как HA addon
|
||
|
||
#### Вариант B: HA Custom Component
|
||
- Кастомная интеграция, регистрирует dynamic sensor-ы
|
||
- Работает как часть HA, доступ к recorder напрямую
|
||
- ✅ **Плюсы:** нативно, без доп. зависимостей
|
||
- ⚠️ **Минусы:** сложнее разработка, перезагрузка при изменениях
|
||
|
||
#### Вариант C: HA Python Scripts + REST Command
|
||
- Python-скрипт через `python_script` integration
|
||
- Запускается по automation, сохраняет результаты через REST
|
||
- ✅ **Плюсы:** без AppDaemon
|
||
- ❌ **Минусы:** `python_script` ограничен в импортах, сложнее batch
|
||
|
||
### Рекомендация: Вариант A (AppDaemon) ✅ ВЫБРАН
|
||
Оптимальный баланс. Изолированный Python-скрипт, batch-обработка, легко масштабировать.
|
||
|
||
**Установлено:** AppDaemon 4.5.13, HA 2026.4.2, Python 3.12.13
|
||
**Путь конфига:** `/addon_configs/a0d7b954_appdaemon/appdaemon.yaml`
|
||
**Путь приложений:** `/addon_configs/a0d7b954_appdaemon/apps/`
|
||
**Путь apps.yaml:** `/addon_configs/a0d7b954_appdaemon/apps/apps.yaml`
|
||
**Slug аддона:** `a0d7b954_appdaemon`
|
||
**Подключение к HA:**
|
||
- REST API: автоматическое через SUPERVISOR_TOKEN (`http://supervisor/core/api`)
|
||
- WebSocket (для registry): HA Long-Lived Access Token через `apps.yaml` аргумент `ha_token` → `ws://homeassistant:8123/api/websocket`
|
||
- ⚠️ SUPERVISOR_TOKEN **не подходит** для прямого WebSocket-подключения к HA — только для REST через supervisor proxy
|
||
|
||
### Первая фаза: только light + switch
|
||
Начинаем с минимального набора — только домены `light` и `switch`. После обкатки расширяем на остальные.
|
||
|
||
**Устройств после фильтрации:** ~32 (2 light + 30 switch)
|
||
|
||
**Дополнительные исключения для switch (настройки реле — не основные устройства):**
|
||
- `*_delayed_power_on_state`
|
||
- `*_detach_relay_mode`
|
||
- `*_network_indicator`
|
||
- `*_turbo_mode`
|
||
- `*_do_not_disturb`
|
||
- `switch.zigbee2mqtt_bridge_permit_join`
|
||
|
||
После этих исключений — ~18 основных устройств.
|
||
|
||
### Cold-start (первичная загрузка и перезапуск)
|
||
При запуске AppDaemon (или после перезапуска HA) — **полный пересчёт текущего активного периода** для всех устройств. Не ждать следующего расписания — данные должны быть актуальны сразу.
|
||
|
||
**Rate-limit при batch-запросах:** 180 устройств × 30 дней истории — ощутимая нагрузка на HA API. Разбивать на батчи по ~20 entity_id с паузой 1 сек между запросами. Показывать прогресс: `sensor.avail_calc_progress` с состоянием `"47/180"` или `"idle"`.
|
||
|
||
## Дашборд
|
||
|
||
### Группировка по комнатам/зонам
|
||
Устройства группируются по **HA areas** (комнатам). Если у устройства нет area — группа «Без комнаты».
|
||
|
||
- Каждая группа — **сворачиваемая секция** с заголовком: `🛏️ Спальня (12 устройств, 97.2%)`
|
||
- В заголовке: иконка комнаты, средняя доступность по группе, количество устройств
|
||
- По умолчанию: проблемные комнаты (средний % < 95%) раскрыты, остальные свёрнуты
|
||
- Сортировка комнат: сначала проблемные, потом по средней доступности (по убыванию)
|
||
|
||
### Мобильная адаптация
|
||
На экранах < 768px (мобильный вид):
|
||
- **Убрать sparkline** — не помещается
|
||
- **Компактная строка:** иконка + friendly name + цветной процент (крупный)
|
||
- **Прогресс-бар** — тонкий, одна линия (не блок)
|
||
- **Сводка падений** — скрыта, раскрывается по тапу (expandable)
|
||
- **Группы комнат** — свёрнуты по умолчанию, тап раскрывает список
|
||
|
||
Десктопный вид — как описано ниже (полный).
|
||
|
||
### Карточка: список устройств с визуалом (десктоп)
|
||
|
||
**Прогресс-бар + сводка для каждого устройства:**
|
||
```
|
||
💡 Бра в спальне ████████████████████░ 97.8%
|
||
📉 100 100 98 95 99 100 98
|
||
5 падений, макс. даунтайм 1ч 42мин
|
||
```
|
||
|
||
**Строка устройства:**
|
||
- **Friendly name** + иконка домена (💡, 🔌, 🌡️ и т.д.)
|
||
- **Прогресс-бар** — заполненность = доступность (%). Цвет по диапазону:
|
||
- 🟢 `≥99%` — зелёный
|
||
- 🟡 `95-99%` — жёлтый
|
||
- 🟠 `90-95%` — оранжевый
|
||
- 🔴 `<90%` — красный
|
||
- **Процент** — справа от прогресс-бара, крупный шрифт
|
||
- **Sparkline** — мини-график из 7 точек под прогресс-баром, тренд стрелкой (📈/📉/➡️)
|
||
- **Сводка падений** — одна строка: `N падений, макс. даунтайм Xч Yмин`
|
||
- **Время последнего падения** — если <24ч: "X минут/часов назад"
|
||
|
||
**Переключатель периода** — вверху дашборда: [24ч] [7д] [30д]. По умолчанию 7д.
|
||
|
||
### Карточка: сводка
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ 📊 Доступность за 7 дней │
|
||
│ │
|
||
│ ██████████████████████░ Средняя: 96.4% │
|
||
│ │
|
||
│ 🔴 Проблемных (<95%): 4 │
|
||
│ 💡 Бра в спальне — 87.2% │
|
||
│ 🔌 Б.колодец насос — 91.3% │
|
||
│ 🌡️ Ванна температура — 82.7% │
|
||
│ 🔒 Замок входной — 94.1% │
|
||
│ │
|
||
│ 📉 Хуже чем прошлую неделю: 3 │
|
||
│ 📈 Лучше чем прошлую неделю: 8 │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
**Функциональность:**
|
||
- Проблемные (<95%) — отдельная сворачиваемая секция
|
||
- Тренд: сравнение с предыдущим периодом (предыдущие 7 дней)
|
||
- Клик на проблемное устройство — скролл к нему в таблице
|
||
- Цветовая индикация средней доступности (прогресс-бар в сводке)
|
||
- Кнопка "Обновить" — принудительный пересчёт
|
||
|
||
## Технические детали
|
||
|
||
### Путь приложения AppDaemon
|
||
```
|
||
/addon_configs/a0d7b954_appdaemon/apps/
|
||
├── apps.yaml # регистрация приложений
|
||
├── availability.py # основной модуль расчёта доступности
|
||
└── availability_utils.py # вспомогательные функции (фильтрация, форматирование)
|
||
```
|
||
|
||
### Источник данных
|
||
- HA History API: `/api/history/period/<start>?filter_entity_id=<ids>&minimal_response&no_attributes`
|
||
- Возвращает массив пар `(state, last_changed)` — достаточно для расчёта времени
|
||
- AppDaemon подключён к HA через WebSocket — использует `self.get_entity_history()` или REST API через `self.hass`
|
||
|
||
### Расчёт (алгоритм)
|
||
```python
|
||
def calc_availability(history_entries, period_start, period_end):
|
||
unavailable_seconds = 0
|
||
for entry in history_entries:
|
||
state = entry['state']
|
||
changed = parse_datetime(entry['last_changed'])
|
||
|
||
if state in ('unavailable', 'unknown'):
|
||
# Сколько времени устройство было в этом статусе
|
||
# до следующего изменения или до period_end
|
||
next_change = get_next_change(entry) or period_end
|
||
unavailable_seconds += (next_change - changed).total_seconds()
|
||
|
||
total_seconds = (period_end - period_start).total_seconds()
|
||
availability = (1 - unavailable_seconds / total_seconds) * 100
|
||
return round(availability, 1)
|
||
```
|
||
|
||
### Хранение результатов
|
||
- Каждый результат — `sensor.avail_<sanitized_id>` (один sensor, период в атрибуте)
|
||
- Атрибуты: `entity_id`, `period`, `availability_pct`, `area`, `down_count`, `max_downtime_minutes`, `sparkline`, `trend`, `last_downtime`, `color`, `last_updated`
|
||
- Группировка по комнатам: `sensor.avail_area_<sanitized_name>`
|
||
- Прогресс расчёта: `sensor.avail_calc_progress` (`"47/180"` или `"idle"`)
|
||
|
||
### Дашборд: имеющиеся кастомные карточки
|
||
Установлены через HACS:
|
||
- **mini-graph-card** — для sparkline
|
||
- **auto-entities** — автоподхват устройств
|
||
- **card-mod** — кастомный CSS (мобильная адаптация)
|
||
- **stack-in-card** — группировка карточек
|
||
|
||
**Нужно доустановить:**
|
||
- **button-card** — для строк устройств с прогресс-баром
|
||
- **custom:hui-element** — для input_select на дашборде (или использовать стандартный entities card)
|
||
|
||
### AppDaemon: реализация
|
||
|
||
**Модули:**
|
||
- `availability.py` — класс `Availability(hass.Hass)`, основная логика
|
||
- `availability_utils.py` — чистые функции (фильтрация, расчёт, форматирование)
|
||
|
||
**Ключевые решения при реализации:**
|
||
- `self.sleep()` в AppDaemon 4.x — coroutine, нельзя вызывать синхронно → заменён на `time.sleep()` (блокирует только worker thread)
|
||
- `log_level: info` в apps.yaml вызывает `ValueError: Unknown level` → убрать, AppDaemon использует INFO по умолчанию
|
||
- HA Registry API (area/entity/device) **недоступен через REST** — только WebSocket (`config/area_registry/list` и т.д.)
|
||
- SUPERVISOR_TOKEN работает для REST через supervisor proxy, но **не для прямого WS** к HA
|
||
- Entity ID не может содержать кириллицу → `sanitize_area_name()` с транслитерацией (а→a, б→b, ...)
|
||
|
||
**apps.yaml:**
|
||
```yaml
|
||
hello_world:
|
||
module: hello
|
||
class: HelloWorld
|
||
|
||
availability:
|
||
module: availability
|
||
class: Availability
|
||
ha_token: <Long-Lived Access Token> # для WebSocket registry
|
||
```
|
||
|
||
**⚠️ Секреты:** `ha_token` хранится в apps.yaml на HA. Не дублировать в других файлах.
|
||
|
||
### Нагрузка
|
||
- ~180 устройств
|
||
- History API batch-запрос (можно передать несколько entity_id через запятую)
|
||
- За день: ~180 × 3 = 540 точек данных (при batch — 3 API-вызова)
|
||
- За неделю/месяц: данные уже рассчитаны, обновляются реже
|
||
|
||
## Ограничения
|
||
- `purge_keep_days` в recorder — по умолчанию 10 дней. Для месячной статистики нужно **увеличить до 35 дней**
|
||
- Если устройство добавлено недавно — показывать доступность с момента добавления (не с начала периода)
|
||
- Если устройство удалено — перестать показывать на дашборде
|
||
|
||
## Что нужно от Славы
|
||
1. ~~Подтвердить вариант реализации~~ → ✅ AppDaemon (Вариант A)
|
||
2. Увеличить `purge_keep_days` до 35 (иначе не будет данных за месяц)
|
||
3. ~~Установить AppDaemon~~ → ✅ Установлен (4.5.13)
|
||
4. ~~Подтвердить список исключений~~ → ✅ Согласовано (см. выше)
|
||
5. Доустановить **button-card** через HACS
|
||
6. ~~Создать `input_select.avail_period`~~ → ✅ Создан (опции: 24h, 7d, 30d, по умолчанию 7d)
|
||
7. Назначить **areas** устройствам в HA (Settings → Areas) — многие показывают «Без комнаты»
|
||
8. Построить **Lovelace-дашборд** (после установки button-card)
|
||
|
||
## Статус деплоя
|
||
|
||
### ✅ Готово
|
||
- AppDaemon 4.5.13 установлен и работает
|
||
- availability.py + availability_utils.py задеплоены
|
||
- 21 device sensor создан (`sensor.avail_*`)
|
||
- 2 area sensor создан (`sensor.avail_area_*`)
|
||
- input_select.avail_period создан
|
||
- WebSocket registry работает (9 areas, 729 entity mappings)
|
||
- Расчёт за все 3 периода (24h/7d/30d)
|
||
|
||
### ⏳ TODO
|
||
- Назначить areas устройствам в HA
|
||
- Увеличить purge_keep_days до 35
|
||
- Установить button-card через HACS
|
||
- Построить Lovelace-дашборд
|
||
- Расширить на другие домены (после обкатки)
|