Files
obsidian-vault/family/how-to/ha-automations.md
T

313 lines
27 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.
# ⚙️ HA — автоматизации
> **Справочник логики автоматизаций** (`automations.yaml` на t610). Топология/команды/Modbus — [[family/how-to/home-automation]].
> 🟢 **25 АВТОМАТИЗАЦИЙ — 24 `on`, 1 `off`** (`Ventilation automation on`, намеренно), `unavailable` = 0. Zigbee работает на ZHA.
>
> **Состав:** 12 контроля батарей (§5) + 2 циркуляция ГВС + 2 свет кабинета + 2 подсветка лестницы + 2 ночной свет душевой + 2 диммер спальни + 1 протечка котельная + 1 вентиляция + **1 греющий кабель ввода воды (§7)**.
>
> **Ключевые факты:**
> - 🔴 **`entity_id` автоматизаций HA перегенерирует по alias** — искать по `attributes.id`, не по `entity_id`.
> - 🔴 **REST `GET/POST /api/config/automation/config/<id>` использует поля во множественном числе** — `triggers`/`conditions`/`actions` (в файле — единственное). Ошибка молча уходит в пустоту: POST → `200 ok`, значения НЕ меняются. **Всегда читать обратно.**
> - ⚠️ **Кнопка спальни** шлёт `remote_button_short_press` только после `zha/devices/reconfigure`.
>
> **Бэкапы перед правками:** `automations.yaml.bak-cable3-20260915-*` (кабель), `.bak-preids-*`, `.bak-gard-*` (единообразие ID). Локальные копии в `~/tmp-t610/`.
> 📄 Рецепт пересборки после Z2M→ZHA и питфоллы — [[family/tech/zigbee-t610-z2m-i-zha]].
>
> 🔴 **ПИТФОЛЛЫ, найденные при починке:**
> 1. **`device`-триггер батареи — тип `battery_level`, НЕ `battery`.** Иначе `Automation ... failed to setup triggers and has been disabled`.
> 2. **`binary_sensor` «battery_low» в ZHA нет** — только numeric `sensor.*_battery`, порог через `below: 10`.
> 3. **Домен `entity_id` не меняется переименованием:** `switch.office_table_light_switch_l1` → ZHA-имя `light.tz3000_5gey1ohx_ts0002_osveshchenie`. Автоматизации переписаны на `light.*`.
> 🍳 **Сменить домен у СУЩЕСТВУЮЩЕЙ сущности HA нельзя** (ни реестром, ни customize). Обход — Template-хелпер поверх исходной сущности. `switch_as_x` для этого не годится: принимает **только `switch`**, не `light`. Разбор — [[family/tech/kitchen-hood-domain-conversion]].
> 4. **`reload` — `POST /api/services/automation/reload`** с long-lived JWT, порт 80 (`http://172.30.32.1/api/`). Супервизорский токен → 401.
## 1. Где живёт
- **Файл:** `/config/automations.yaml` в HA Core на t610.
- **Всего:** **25 автоматизаций — 24 `on`, 1 `off`** (`Ventilation automation on`, намеренно), **`unavailable` = 0**. Файл = 780 строк.
- **Бэкапы перед правками:** `automations.yaml.bak-cable3-<ts>` (кабель, 2026-09-16), `.bak-preids-*`, `.bak-gard-*`; локально — `~/tmp-t610/bak-cable-20260915-234837/automations.yaml`.
- **Удалены 4 «призрака»** (записи в реестре без тела): `svetlo_vykl_osveshchenie_lestnitsy`, `temno_vkl_podsvetku_lestnitsy`, `datchik_osveshchennosti_lestnitsa_batareia`, `light_switch_bed_batareia`. Тела не было (`config/automation/config/<id>` → 404) — лестничные сценарии **пересозданы заново** с теми же id.
- **Локальная копия:** `~/tmp-t610/automations/automations.yaml`; после пересборки — `~/tmp-t610/automations-new.yaml`, `~/tmp-t610/automations-fixed.yaml`.
- **Читать живьём через API** (не по памяти):
```bash
B="https://mallexxx.duckdns.org"
curl -s -H @/tmp/h1 "$B/api/config/automation/config/<ID>"
```
### 🔴 ПИТФОЛЛ (стоил пустой заливки)
REST `GET/POST /api/config/automation/config/<id>` использует поля **во множественном числе — `triggers` / `conditions` / `actions`** (в файле `automations.yaml` — `trigger`/`condition`/`action`).
Правка по единственному числу **молча уходит в пустые пути**: POST → `200 {"result":"ok"}`, значения НЕ меняются.
→ **ВСЕГДА читать конфиг обратно и сверять фактические значения.**
**Бэкап перед правкой:** `cp ~/tmp-t610/automations/automations.yaml{,.bak-$(date +%Y%m%d-%H%M%S)}`
---
## 2. Карта автоматизаций
| id | alias | Логика |
|---|---|---|
| `1768585827761` | Выключить циркуляцию ГВС | `time 23:00` → turn_off |
| `1768585922970` | Включить циркуляцию ГВС | `time 09:30` → turn_on |
| `1770404069135` | Ventilation automation on | `time 05:00` → `fan.turn_on fan.automation` *(off)* |
| `5735cb9f855e462dbdbdf680d0d6a66f` | **office_pass_switch_table** | кнопка L1 → toggle `light.smart_light_office_left` ⚠️ |
| `45b96f6f38f6488ba70266fa5da665f5` | **office_pass_switch_main** | кнопка L2 → toggle `light.smart_light_office_right` ⚠️ |
| `1771466806839` | Темно: вкл. подсветку лестницы | `illuminance below: 20` → `light.light_stairs_left`+`_right` turn_on |
| `1771466955010` | Светло: выкл. подсветку лестницы | `illuminance above: 60` → `light.light_stairs_left`+`_right` turn_off |
| `1771683420621` | Toggle Dimmer bed | кнопка `remote_button_short_press` → `light.toggle light.bed_dimmer` |
| `1771683677259` | Dimmer bed cycle | кнопка `long_press` → `light.turn_on` brightness 50 % на `light.bed_dimmer` |
| `1771997851260` | **Вкл. ночной свет душевая** | присутствие + темно → `light.dushevaia_night_light` on |
| `1771997918348` | **Выкл. ночной свет душевая** | нет присутствия / светло → off |
| `1773451257968` | Протечка котельная | `moist` → `notify.notify` |
| `8800000000000`…`8800000000011` | **Батарея: 12 шт.** | см. §6 |
| `heating_cable_ctl_0001` | **Греющий кабель: управление** | `time_pattern /15` + `numeric_state ZONT below 8` + отвал датчика → `choose` 5 веток на `switch.heating_cable_plug`. См. §7 |
> 🔄 **Переименовано 2026-09-15:** ссылки на `light.night_light_shower_2` → `light.dushevaia_night_light` (id `1771997851260`, `1771997918348`).
> ⛔ **Удалены из карты:** `1773451323415` «Датчик протечки котельная батарея» и `1773459663218` «Zigbee T sensor батарея» — заменены 12 единообразными (§6). Тела удалены, осиротевшие записи реестра вычищены.
> ⚠️ **Два сценария подсветки лестницы (id `1771466806839` / `1771466955010`) удалены как «призраки» и ПЕРЕСОЗДАНЫ** с теми же id на `sensor.light_sensor_stairs_illuminance`. Пороги 20/60.
> 🗑 **Удалённые «призраки», которых в файле НЕТ:** `automation.svetlo_vykl_osveshchenie_lestnitsy`, `automation.temno_vkl_podsvetku_lestnitsy`, `automation.datchik_osveshchennosti_lestnitsa_batareia`, `automation.light_switch_bed_batareia`.
> **Образец правильной защиты от дребезга:** «Темно/Светло подсветка лестницы» использует **гистерезис** (`below: 20` / `above: 60`). Брать как эталон.
### 2.1. 📊 Разбор: почему ВЫКЛ сработал «рано» (2026-09-16, 07:50 местного)
**Вопрос Alex:** «Почему сценарий выключения подсветки лестницы уже сработал? Разве уже достаточно светло?»
**Ответ: сработал ПРАВИЛЬНО. Причина — рассвет, не лампа.**
| UTC | Местное (UTC+7) | lx | Событие |
|---|---|---|---|
| 23:46 (15.09) | 06:46 | 2 | рассвет начался, датчик пошёл с нуля |
| 00:49:37 | 07:49 | 46 | последнее «тёмное» значение |
| 00:50:21 | 07:50 | 54 | |
| 00:50:33 | 07:50 | 59 | |
| 00:50:45 | 07:50 | **63** | **порог 60 пересёкся → `light.turn_off`** |
| 00:50:46 | 07:50 | — | `light.light_stairs_left` + `_right` → `off` ✅ |
| 00:54:23 | 07:54 | 103 | текущее |
**Как отличить рассвет от лампы по истории:** за 6 ч датчик прошёл **0 → 2 → 7 → 10 → 13 → … → 63** — ровный монотонный подъём. Лампа дала бы **скачок в сотни люкс за секунды**. Плюс `last_triggered` ВЫКЛ = `00:50:45.546`, а лампа выключилась в `00:50:46` — совпадение до миллисекунд.
**Диагностика (воспроизводимо):** `~/tmp-t610/stairs_ctx.sh` на t610 — состояние + история 6 ч + `last_triggered` обеих автоматизаций за один прогон.
> ⚠️ **Остаточный риск (не залипание, а дребезг):** окно гистерезиса **20–60 lx узкое по времени** — здесь переход 59→63 занял **24 секунды**. В пасмурную погоду или при медленном рассвете (после 21–22 сентября день укорачивается) освещённость будет **колебаться вокруг порога**, и подсветка начнёт хлопать: каждое падение <20 → вкл, каждый подъём >60 → выкл.
> **Предложенный фикс (ждёт решения Alex — правка боевого конфига):** расширить окно до **`below: 15` / `above: 80`**. Тогда утренний переход — секунды, а не минуты, и дребезг исключён.
> 📌 На 2026-09-16 **не залипло и не хлопало** — фикс превентивный, не срочный.
---
## 3. 🔴 ДЕФЕКТ: свет кабинета мигает при перезагрузке HA
**Симптом:** при каждом рестарте HA свет в кабинете мигает.
**Первопричина:** триггеры `office_pass_switch_table` / `office_pass_switch_main` используют `platform: state` **без `to`/`from`** → срабатывают на **любую** смену состояния сущности кнопки.
При старте HA кнопки проходят `unavailable → unknown → on` — каждый переход дёргает `light.toggle`. Подтверждено историей: `switch.office_table_light_switch_l1` сменил состояние 3 раза в течение 5 минут в момент рестартов HA (`05:16:36 → unavailable`, `05:16:40 → on`, `05:18:39 → unavailable`, `05:18:46 → unknown`, `05:19:20 → on`).
**Текущий конфиг (сырой):**
```json
{
"alias": "office_pass_switch_table",
"mode": "restart",
"trigger": [{"platform": "state", "entity_id": ["switch.office_table_light_switch_l1"]}],
"action": [{"service": "light.toggle", "target": {"entity_id": "light.smart_light_office_left"}}]
}
```
**Направление фикса:** привязать триггер к конкретному состоянию (`to:`) ИЛИ заменить на триггер по событию нажатия. Требует определить фактическое поведение кнопки при физическом нажатии (сейчас кнопки в `on` — это состояние после старта, не нажатие).
**Статус:** ⚠️ **НЕ ИСПРАВЛЕНО.** Требует замера поведения кнопки при нажатии.
---
## 4. Ночной свет душевой 2
**Сущности** (device_id `4095e7c3b47b9dc9640cfb8c3aeff022` = радар, `4d6e55505ff7dbad13d2674cdcb18d5a` = лампа):
- `sensor.shower_2_presence_sensor_illuminance` — освещённость, lx
- `binary_sensor.shower_2_presence_sensor_presence` — занятость
- `light.dushevaia_night_light` — **само реле** (его дёргает автоматизация; бывш. `light.night_light_shower_2`)
> 📌 **2026-09-15:** хелпер `switch.night_light_shower_2` (`switch_as_x`) снесён при переезде на ZHA. Осталась одна сущность — `light.dushevaia_night_light`. Автоматизации ссылаются именно на неё.
**Автоматизации:**
| | id | Триггер | Условие |
|---|---|---|---|
| ВКЛ | `1771997851260` | `illuminance below: 6` + `occupied` | `below: 6` + `is_occupied` |
| ВЫКЛ | `1771997918348` | `not_occupied` + `illuminance above: 6` | ⚠️ **`conditions: []`** — пусто |
**История правки (2026-09-15):** порог `8 → 6`. Порог `8` лежал ровно в центре дребезга датчика (**7 ↔ 12**), рабочий диапазон датчика всего **0…15 lx** → ВКЛ/ВЫКЛ хлопали по кругу.
**⚠️ Остаточный риск (принят Alex):** при **2 lx** (покой) ВЫКЛ ждёт `above: 6` и может не наступить → свет залипнет. Гистерезис сознательно не сделан.
**Если залипнет — варианты:** ① гистерезис `вкл below 4 / выкл above 10`; ② гасить по факту включения основной лампы (**блокер: сущность основного освещения верхней душевой неизвестна**); ③ калибровка `illuminance_calibration` радара.
**Параметры радара:** `fading_time` = 10 с, `maximum_range` = 2.85 м — проверять, покрывает ли зона всю душевую.
---
## 5. 🔋 Контроль батарей — 12 автоматизаций (2026-09-15)
**Единый шаблон**, id `8800000000000`…`8800000000011`:
```yaml
- id: '8800000000000'
alias: 'Батарея: <место>'
description: 'Контроль заряда Zigbee-датчика. Порог 20%, выдержка 2ч.'
triggers:
- trigger: numeric_state
entity_id: sensor.<device>_battery
below: 20
for: '02:00:00'
id: low
- trigger: numeric_state
entity_id: sensor.<device>_battery
above: 20
id: ok
conditions: []
actions:
- choose:
- conditions: [{condition: trigger, id: low}]
sequence:
- action: persistent_notification.create
data:
title: '🔋 Батарея разряжена'
message: '<место> — заряд {{ states(''sensor.<device>_battery'') }}%. Замените батарейку.'
notification_id: bat_<slug>
- action: notify.mobile_app_sm_s931b
data:
title: '🔋 Батарея разряжена'
message: '<место> — заряд {{ states(''sensor.<device>_battery'') }}%. Замените батарейку.'
- conditions: [{condition: trigger, id: ok}]
sequence:
- action: persistent_notification.dismiss
data: {notification_id: bat_<slug>}
mode: single
```
| id | Место | Сущность заряда |
|---|---|---|
| `8800000000000` | Туалет 1 этаж (тёплый пол) | `sensor.toilet_1_floor_temperature_battery` |
| `8800000000001` | Лестница (освещённость) | `sensor.light_sensor_stairs_battery` |
| `8800000000002` | Душевая (тёплый пол) | `sensor.dushevaia_floor_temperature_battery` |
| `8800000000003` | Котельная (протечка) | `sensor.boiler_water_leak_battery` |
| `8800000000004` | Гардеробная (температура) — датчик перенесён из Кабинета 2026-09-15 | `sensor.garderobnaia_temperature_battery` |
| `8800000000005` | Гостиная (тёплый пол) | `sensor.living_room_floor_temperature_battery` |
| `8800000000006` | Серая (тёплый пол) | `sensor.severnaia_floor_temperature_battery` |
| `8800000000007` | Кабинет (тёплый пол) | `sensor.kabinet_floor_temperature_battery` |
| `8800000000008` | Кухня (тёплый пол) | `sensor.kitchen_floor_temperature_battery` |
| `8800000000009` | Ванная (тёплый пол) | `sensor.vannaia_floor_temperature_battery` |
| `8800000000010` | Прихожая (тёплый пол) | `sensor.prikhozhaia_floor_temperature_battery` |
| `8800000000011` | Спальня (кнопка) | `sensor.wireless_light_switch_bed_battery` |
### 5.1. 🔴 Питфоллы
1. **Выдержка `for: '02:00:00'` обязательна.** Батарейные TS0201 под нагрузкой дают кратковременные просадки — без выдержки прилетают ложные алерты.
2. **Двойной канал.** `persistent_notification` = видно в UI; `notify.mobile_app_sm_s931b` = push на телефон. Alex: «Видимо persistent с пушом на тел».
3. **Второй триггер `ok` (`above: 20`) гасит уведомление** через `notification_id` — иначе алерт висит вечно. Для этого у `persistent_notification.create` обязателен `notification_id: bat_<slug>`.
4. 🔴 **Сирота-автоматизация: `unavailable` при чистом YAML.** Тело удалено из `automations.yaml`, запись в реестре осталась. Удаление:
```python
# Если config/entity_registry/remove падает с id_reuse:
# "Identifier values have to increase"
{"type":"config/entity_registry/update","entity_id": E, "disabled_by": "user"} # сначала
{"type":"config/entity_registry/remove","entity_id": E} # потом
```
Реальный случай: после удаления тел `1773451323415` / `1773459663218` обе сущности остались `unavailable`.
5. ⚠️ **`sensor.wireless_light_switch_bed_battery` = `unknown`** — кнопка спальни спит. Автоматизация создана, но триггера не будет, пока датчик не проснётся (лечится нажатием кнопки).
6. ⚠️ **Проверять имена сущностей после переименований:** в ZHA домен бывает `_batareia`, но `trigger: battery_level` (не `battery`).
7. 🔴 **`id_reuse: Identifier values have to increase` при переименовании сущности** (проверено 2026-09-15: 6 из 7 упало, хотя целевые `entity_id` свободны). Это внутренний счётчик реестра, **не поломка**. Обход — промежуточное имя:
```python
{"type":"config/entity_registry/update","entity_id": old, "new_entity_id": tmp} # dom.tmp_xxx
{"type":"config/entity_registry/update","entity_id": tmp, "new_entity_id": new}
# при провале 2-го шага — откат: tmp → old
```
Общий принцип: **любая операция реестра, падающая с `id_reuse`, лечится промежуточным состоянием** (для сирот-автоматизаций — `disabled_by: user`, см. питфолл 4).
8. 🔴 **HA перегенерирует `entity_id` автоматизаций по alias при `reload`.** Проверять/искать по `attributes.id`, не по `entity_id`. Пример: `automation.datchik_protechki_kotelnaia_batareia` → `automation.batareia_kotelnaia_datchik_protechki`.
**Скрипты:** `~/tmp-t610/add_battery_autos.py` (генератор), `~/tmp-t610/rm_orphans.py`, `rm_orphan2.py`, `rm_orphan3.py`, `gard_fix.py` (правка ссылок при переносе датчика), `fix_aut_entid.py` (правка `entity_id` автоматизации).
> ⚠️ **ОТКРЫТО (на 2026-09-15):** Alex сообщил, что HA ругается на «**Office temperature sensor battery** — нет уникального id, устройство недоступно». **Проверено: призрака в бэкенде НЕТ** (нет в `/api/states`, нет в реестре из 572 записей, MQTT-сущностей без `unique_id` — 0, config entries все `loaded`). Вероятный источник — UI-кэш или дашборд-карточка. **Нужно уточнить у Alex экран** (Настройки → Устройства → MQTT / дашборд / Developer Tools) и вычистить прицельно.
> 📄 Полный отчёт о работах: [[family/plans/t610-zigbee-ids-battery-freshsensors-modbus]]
---
## 6. Диагностика
```bash
# История значения (доказательство дребезга)
T=$(date -u -v-3H '+%Y-%m-%dT%H:%M:%S')
curl -s -H @/tmp/h1 "$B/api/history/period/$T+00:00?filter_entity_id=<entity>&minimal_response&no_attributes" \
| jq -r '.[0][] | "\(.last_changed) -> \(.state)"'
# Состояния сущностей устройства (через шаблон)
curl -s -X POST -H @/tmp/h1 -H "Content-Type: application/json" \
-d '{"template":"{% set dev = device_id(\"sensor.x\") %}{% for e in device_entities(dev) %}{{ e }} = {{ states(e) }}\n{% endfor %}"}' \
"$B/api/template"
```
**Артефакты:** `~/tmp-t610/fix_shower_light_threshold.sh` (образец правки порога), `~/tmp-t610/ha_ws.py` (WS-реестры).
**Бэкап:** `~/tmp-t610/automations/automations.yaml.bak-20260915-105455`.
---
## 7. Греющий кабель: управление (2026-09-16)
**Автоматизация:** `automation.greiushchii_kabel_upravlenie` · id `heating_cable_ctl_0001` · `mode: single` · **одна** на все случаи (0 helper'ов).
**Управляет:** `switch.heating_cable_plug` (NEO NAS-WR01B, `a4c138eb6fbe9d19`, котельная) — розетка греющего кабеля ввода воды. Кабель саморегулирующийся.
**Порог −8 °C** — по расчёту промерзания бетонной подушки 30 см (Новосибирск, суглинок, труба на 4 м). Выдержка 12 ч = тепловая инерция бетона. Прогноз привлекается потому, что ZONT — одна точка и может отвалиться.
### Ветки логики
| Ветка `choose` | Условие | Действие |
|---|---|---|
| 1 | `eff ≤ 20` | `switch.turn_on` + persistent + push (без выдержки) |
| 2 | `not zont_ok` | `switch.turn_on` + push (fail-safe) |
| 3 | `eff ≤ 8` + розетка `off` **12 ч** | `switch.turn_on` + persistent + push |
| 4 | `eff ≥ 3` + розетка `on` | `switch.turn_off` + persistent |
| 5 | розетка `on` + `power < 5 W` 10 мин | push «нет потребления» |
Триггеры: `time_pattern /15` + `numeric_state ZONT below 8` + `state ZONT → unknown/unavailable 30 мин`.
где `eff` = ZONT-улица, если датчик жив, иначе прогноз-мин на 24 ч.
**Конструкция — прогноз как ДЕЙСТВИЕ, не как шаблон:**
```yaml
actions:
- action: weather.get_forecasts # сервис во МНОЖЕСТВЕННОМ числе
target: {entity_id: weather.forecast_laki_dom}
data: {type: hourly}
response_variable: wx
- variables: # ПОСЛЕ вызова — forecast уже есть
zont_ok: "{{ states('sensor.h2000_pro_temperatura_ulitsa') not in ['unknown','unavailable','none',''] }}"
fc_min: "{{ wx['weather.forecast_laki_dom']['forecast'][:24] | map(attribute='temperature') | min | float(-100) }}"
eff: "{{ zont if zont_ok else fc_min }}"
- choose: [...] # 5 веток
```
> 🔴 **Питфоллы этой автоматизации (все три поймал проверкой, без неё ушли бы в прод):**
> 1. **`weather.get_forecasts` — ТОЛЬКО как `action:` + `response_variable`.** В Jinja-шаблоне → `'weather' is undefined`; атрибута `forecast` у сущности нет. Имя — **множественное число**; `weather.get_forecast` (ед. ч.) не существует.
> 2. **`variables:` вычисляются ДО действий.** `fc_min` из `response_variable` можно объявить **только внутри `actions:`, после** вызова сервиса.
> 3. **Fail-safe нельзя строить на `min([99, fc_min])`** — `min([99, 11.1])` = 11.1, условие не сработает никогда. Нужен отдельный флаг `zont_ok` по строковому состоянию.
>
> ⚠️ **Выдержка 12 ч** реализована через `for: '12:00:00'` на `switch.heating_cable_plug` (состояние), НЕ на шаблон — `for:` нельзя вешать на вычисляемое значение.
> ⚠️ **WS `render_template` возвращает `null`** даже для `{{ 2 + 2 }}` — шаблоны проверять только через REST `POST /api/template`.
> ⚠️ **`automation.trigger` через WS не обновляет `last_triggered`** — прогон подтверждать иначе (значения в `variables`, лог HA).
📄 **План (выполнен):** `family/plans/t610-heating-cable-automation.md`
🧰 **Инструменты:** `~/tmp-t610/ha_ws.py forecast <entity> <daily|hourly>`, `verify_cable.py`, `verify_calc.sh`, `rest_tpl.sh`.
### ⚠️ Не проверено физически
Розетка ни разу не включалась — `off`, `0 W / 0 kWh`, `voltage 223 V`. Это **норма** для выключенной розетки, не поломка. Что кабель реально греет — не подтверждено: нужен ручной прогон `switch.heating_cable_plug` на 10 мин и проверка `sensor.heating_cable_plug_power > 0`.
---
## Связанные
- [[family/how-to/home-automation]] — единый справочник: топология, железо, Zigbee, Modbus, команды, сценарии, питфоллы
- [[family/documents/home-automation-wishlist]] — роадмап идей и приоритетов
- [[family/how-to/vault-sync-pipeline]] — правки в vault не доедут до телефона без прогона sync-петли