diff --git a/family/how-to/ha-automations.md b/family/how-to/ha-automations.md index bda94465..78bee951 100644 --- a/family/how-to/ha-automations.md +++ b/family/how-to/ha-automations.md @@ -267,6 +267,56 @@ trigger: ## 4. Ночной свет душевой 2 +> 🔴 **Главный разбор — §4.4** (`condition` внутри `sequence` не отменяет остальные `actions`). Читать до правок этой автоматизации. + +### 4.4. 🔴 КОРЕНЬ ВСЕХ БАГОВ: `condition` внутри `sequence` не отменяет остальные `actions` (2026-09-16) + +**Симптом:** свет зажигался после выхода из душевой, несмотря на проверку присутствия. Задержка 2→3→5 с не помогала, `fading_time` 10→2 не помогал. + +**Первопричина — доказана трассировкой HA (`14:02:38`):** +``` +14:02:38.426597 action/0/choose/0/sequence/1/entity_id/0 + {"result": false, "state": "off", "wanted_state": "on"} ← проверка ОТБИЛА +14:02:38.427615 action/1 → light.turn_on ← ВКЛЮЧИЛ +``` + +> 🔴 **`condition:` внутри `sequence:` прерывает ТОЛЬКО свою `sequence`.** Управление возвращается в родительский список `actions` — и выполняются следующие шаги. Если `light.turn_on` стоит **после** `choose` (на верхнем уровне `actions`), он выполнится **всегда**, независимо от результата проверки. + +**Сломанная структура (моя ошибка, 3 итерации):** +```yaml +actions: +- choose: + - conditions: [trigger lux] + sequence: + - {delay: 5} + - {condition: state, presence == on} # false → вышли из sequence + default: [] +- action: light.turn_on # ❌ выполняется ВСЕГДА +``` + +**Правильная структура — действие ВНУТРИ ветки:** +```yaml +actions: +- choose: + - conditions: [{condition: trigger, id: presence}] + sequence: [{action: light.turn_on, ...}] # заход → мгновенно + - conditions: [{condition: trigger, id: lux}] + sequence: + - {delay: {seconds: 5}} + - {condition: state, entity_id: presence, state: "on"} + - {action: light.turn_on, ...} # ✅ внутри, после проверки + default: [] +``` + +**Верификация:** конфиг прочитан обратно — оба `light.turn_on` внутри `choose`; автоматизации 25/24 `on`/1 `off`, `unavailable` = 0. +**Бэкап:** `/config/automations.yaml.bak-shower-final-20260916-211051`. +**Скрипт:** `~/tmp-t610/fix_shower_final.sh`. + +> 🔴 **ПИТФОЛЛ ДИАГНОСТИКИ: трассировки HA — единственный способ увидеть реальные значения условий.** Доступны **только по WebSocket** (`trace/list` + `trace/get`), REST отдаёт 404. `item_id` = **внутренний ID автоматизации** (`1771997851260`), **не** `entity_id`. Файл `.storage/trace.saved_traces` содержит только вручную сохранённые через UI, живые трассировки — в памяти. +> 🧰 Инструмент: `~/tmp-t610/trace_dump.py` (Mac, python3 + `websocket-client`). На t610 python3 НЕТ. + +--- + **Сущности** (device_id `4095e7c3b47b9dc9640cfb8c3aeff022` = радар, `4d6e55505ff7dbad13d2674cdcb18d5a` = лампа): - `sensor.shower_2_presence_sensor_illuminance` — освещённость, lx @@ -413,7 +463,9 @@ mode: single --- -## 4.4. 📋 Разбор: ложные включения при выходе (feedback loop, 2026-09-16) +## 4.6. 📋 Разбор: ложные включения при выходе (2026-09-16) — предыстория + +> **✅ ЗАКРЫТО.** Корень найден трассировкой — см. **§4.5**, итоговая конструкция — **§4.4** (действие внутри ветки `choose`). Ниже сохранён исходный разбор как контекст. **Симптом (Alex):** «выхожу, выключаю свет, через 2 секунды включается подсветка хоть и не должна». @@ -453,46 +505,42 @@ mode: single --- -## 4.5. ✅ ИТОГ: фиксированная задержка заменена на `wait_for_trigger` (2026-09-16) +## 4.5. ⛔ ЧТО НЕ СРАБОТАЛО: полная хронология перебора (2026-09-16) -**Хронология перебора — что НЕ сработало и почему:** +> **Итоговое решение — §4.4** (действие **внутри** ветки `choose`). Ниже — что перебиралось и почему каждый вариант провалился. -| Попытка | Конструкция ветки `lux` | Результат | +| # | Конструкция ветки `lux` | Результат | |---|---|---| | 1 | `delay: 2 s` + `condition: device / is_occupied` | свет зажигался через ~3 с ❌ | | 2 | `delay: 3 s` + `is_occupied` | зажигался через 5 с ❌ | | 3 | `delay: 5 s` + `is_occupied` | зажигался через 5 с ❌ | | 4 | `wait_for_trigger from: on to: off` + `{{ wait.trigger is not none }}` + `condition: state == on` | **противоречивая конструкция** — требует presence и `off`, и `on` одновременно → свет не включался бы вообще ❌ (поймано до прода) | | 5 | `delay: 5 s` + `condition: state == on` | зажигался ❌ | -| 6 | `wait_for_trigger to: off`, `timeout: 6 s` + `{{ wait.trigger is none }}` | ✅ **применено** | +| 6 | `wait_for_trigger to: off`, `timeout: 6 s` + `{{ wait.trigger is none }}` | зажигался ❌ (это поймала трассировка) | +| 7 | **`light.turn_on` внутри той же ветки `choose`** | ✅ **итог — §4.4** | -**Почему таймер не работал, хотя арифметика сходилась.** Замеренные окна в циклах Alex: `13:58` — lux-триггер `13:58:10.54`, presence `off` в `13:58:13.73` (окно **3.19 с**); `14:00` — окно **3.59 с**; `14:02` — окно **3.19 с**. Задержка 5 с формально перекрывала все три, проверка `condition: state` при `presence = off` **работает** (проверено изолированным тестом: при `presence = off` свет не включается). Тем не менее включение происходило. -**Накладные расходы:** замер `call → свет on` при `delay: 5` дал **6.1 с** (≈1.1 с на диспетчеризацию). Т.е. фактический момент проверки плавает и таймером его надёжно не поймать. +**Почему варианты 1–6 провалились, хотя арифметика сходилась.** Замеренные окна в циклах Alex: `13:58` — lux-триггер `13:58:10.54`, presence `off` в `13:58:13.73` (окно **3.19 с**); `14:00` — **3.59 с**; `14:02` — **3.19 с**. Задержка 5 с формально перекрывала все три, а проверка `condition: state` при `presence = off` **действительно работает** (проверено изолированным тестом: при `presence = off` свет не включается). -> 🔴 **ВЫВОД (главный урок): для «дождаться, пока датчик отпустит» фиксированный таймер принципиально ненадёжен — момент проверки плавает из-за накладных, а окно отпускания зависит от того, сколько Alex двигался. Нужен триггер по СОБЫТИЮ, а не по времени.** +**Настоящая причина — не в условиях, а в структуре `actions`.** Трассировка `14:02` показала: -**Применённая конструкция (ветка `lux`):** -```yaml -- wait_for_trigger: - - {platform: state, entity_id: binary_sensor.shower_2_presence_sensor_presence, to: "off"} - timeout: {seconds: 6} - continue_on_timeout: true -- condition: template - value_template: "{{ wait.trigger is none }}" # сработал → кто-то ушёл → ОТБОЙ -mode: restart +``` +14:02:38.426597 action/0/choose/0/sequence/1/entity_id/0 + {"result": false, "state": "off", "wanted_state": "on"} ← проверка ОТБИЛА +14:02:38.427615 action/1 → light.turn_on ← ВКЛЮЧИЛ ``` -Семантика: если за 6 с присутствие **пропало хоть на миг** — включать не будем. Если держалось всё время — включаем. +`condition` внутри `sequence` прерывает **только свою `sequence`**; управление возвращается в родительский список `actions`, где стоял `light.turn_on` **вне** `choose` — и выполнялся всегда. Полный разбор — **§4.4**. -> 🔴 **ПИТФОЛЛ: не строить `wait_for_trigger` с взаимоисключающими условиями.** Вариант `from: on, to: off` + следующее условие `presence == on` — логически невыполним (ждём уход и одновременно требуем присутствие). Свет по такой ветке не включится **никогда**. Проверять выполнимость цепочки до заливки в прод. -> ⚠️ **`wait.completed` vs `wait.trigger`:** `wait.completed` = `true` при срабатывании, `false` при таймауте → для «дождаться ухода» нужно `{{ not wait.completed }}` или `{{ wait.trigger is none }}`. В сценарии ВЫКЛ (§4.2) семантика обратная — там нужен `{{ wait.completed }}` (ждём возврат). +> 🔴 **Главный урок (главнее любых задержек):** порядок `choose` → «условие» → **действие снаружи** ломает всю логику молча. Проверка может вернуть `false`, и действие всё равно выполнится. **Действие обязано быть внутри той же ветки, после проверки.** +> 🔴 **Второй урок, про диагностику:** три итерации ушли на перебор задержек, потому что я не читал **трассировку автоматизации**. Она лежала в HA всё время и сразу показала `result: false` перед включением. Трассировки — **только WebSocket** (`trace/list` + `trace/get`), `item_id` = внутренний ID. Инструмент: `~/tmp-t610/trace_dump.py`. -**Метод диагностики, который дал ответ (воспроизводимо):** сопоставление трёх рядов в одном окне — `history/period` по `presence` + `illuminance` + `light` и `logbook` по обеим автоматизациям с полем `message` (`triggered by numeric state of ...` показывает, **какая ветка** сработала). Без `message` версию с `presence` и `lux` не различить. +> 🔴 **ПИТФОЛЛ: не строить `wait_for_trigger` с взаимоисключающими условиями.** Вариант 4 (`from: on, to: off` + требование `presence == on`) логически невыполним. Проверять выполнимость цепочки до заливки в прод. +> ⚠️ **`wait.completed` vs `wait.trigger`:** `wait.completed` = `true` при срабатывании, `false` при таймауте. В сценарии ВЫКЛ (§4.2) нужен `{{ wait.completed }}` (ждём возврат); для «дождаться ухода» — `{{ wait.trigger is none }}`. -**Бэкапы:** `.bak-shower-delay3-20260916-205557`, `.bak-shower-delay5-20260916-205953`, `.bak-shower-wait-20260916-*`, `.bak-shower-waitoff-20260916-*`. -**Скрипты:** `~/tmp-t610/fix_shower_delay3.sh`, `fix_shower_delay5.sh`, `fix_shower_waitrelease.sh`, `fix_shower_state_cond.sh`, `fix_shower_waitoff.sh`; тесты — `test_state_condition.sh` (проверка, что `condition: state` отбивает), `test_trigger_lag.sh` (замер накладных), `check_shower_timing.sh` (три ряда истории). +**Метод диагностики, который дал ответ:** сопоставление трёх рядов в одном окне — `history/period` по `presence` + `illuminance` + `light` и `logbook` по обеим автоматизациям с полем `message` (`triggered by numeric state of ...` показывает, **какая ветка** сработала) — **плюс `trace/get` по WebSocket**, который вскрыл корень. -> ⏳ **Ждёт подтверждения Alex.** Если зажжётся и при этой конструкции — значит модель последовательности действий (кто/когда двигается) понята неверно, и нужен замер с секундомером от самого Alex. +**Бэкапы:** `.bak-shower-delay3-*`, `.bak-shower-delay5-*`, `.bak-shower-wait-*`, `.bak-shower-waitoff-*`, `.bak-shower-final-20260916-211051`. +**Скрипты:** `~/tmp-t610/fix_shower_final.sh` (итог), `fix_shower_delay3.sh`, `fix_shower_delay5.sh`, `fix_shower_waitoff.sh`; тесты — `test_state_condition.sh`, `test_trigger_lag.sh`, `check_shower_timing.sh`; трассировки — `trace_dump.py`, `trace_read.py`. --- diff --git a/family/tech/zigbee-t610-z2m-i-zha.md b/family/tech/zigbee-t610-z2m-i-zha.md index c62b30ba..f069adb5 100644 --- a/family/tech/zigbee-t610-z2m-i-zha.md +++ b/family/tech/zigbee-t610-z2m-i-zha.md @@ -411,6 +411,8 @@ curl -s -H "$HDR" http://supervisor/backups | jq | 31 | 🔴 **`condition: state` с числовым порогом не принимает `below`** | POST → `Message malformed: not a valid option at 'conditions[0].below'`. Порог освещённости — только device-условием `type: is_illuminance`. Ошибка валидации конфиг НЕ портит (проверено) | | 32 | 🔴 **`automation.trigger` НЕ подставляет `trigger.id`** | Прогон через него всегда уходит в `choose.default` — ветку по `trigger.id` так не протестировать. Проверять временным скриптом: `POST /api/config/script/config/` → `script/reload` → вызов → `DELETE`. ⚠️ `delay` живёт только в `actions:` и не знает, какой триггер сработал — ветки различать через `trigger.id` + `choose` | | 33 | 🔴 **`fading_time` радара `_TZE204_qasjif9e` (TS0601) не опускается ниже 2 с** | HA показывает `min: 1` и отдаёт `state: 1.0` в ответе `number.set_value`, но сущность откатывается к `2.0`. Проверено: `1.0` → `2.0`, `1.5` → `2.0`, `2.5` → `2.5`. Прошивка молча игнорирует < 2. **Всегда читать обратно** | +| 34 | 🔴 **`condition:` внутри `sequence:` НЕ отменяет остальные `actions`** | Прерывает только свою `sequence`; управление возвращается в родительский список `actions` и выполняет следующие шаги. `light.turn_on` **после** `choose` выполнится ВСЕГДА, даже если проверка вернула `false`. Действие обязано быть **внутри** ветки, после проверки. Доказано трассировкой: `sequence/1/entity_id/0 → {"result": false}` и через 1 мс `action/1 → light.turn_on`. Разбор: [[family/how-to/ha-automations]] §4.4 | +| 35 | 🔴 **Трассировки автоматизаций — ТОЛЬКО WebSocket** | REST (`/api/trace/...`) → 404. Команды `trace/list` + `trace/get`, домен `automation`. 🔴 `item_id` = **внутренний ID** (`1771997851260`), **НЕ** `entity_id`. `.storage/trace.saved_traces` хранит только сохранённые вручную через UI; живые — в памяти. 🧰 `~/tmp-t610/trace_dump.py` (Mac; на t610 python3 НЕТ). **Читать трассировку ДО перебора гипотез** | | 34 | 🔴 **Фиксированный `delay` ненадёжен для «дождаться, пока датчик отпустит»** | Момент проверки плавает: замер `call → свет on` при `delay: 5` дал **6.1 с** (≈1.1 с накладных на диспетчеризацию). Окно отпускания радара тоже плавает (замерено 3.19–3.59 с). Таймер `2 → 3 → 5 с` в ветке `lux` душевой **не сработал ни разу**. Использовать `wait_for_trigger` по событию: `- {platform: state, entity_id: ..., to: "off"}` + `timeout` + `continue_on_timeout: true` + `{{ wait.trigger is none }}`. Разбор: [[family/how-to/ha-automations]] §4.5 | | 35 | 🔴 **`wait_for_trigger` с взаимоисключающими условиями — свет не включится никогда** | Ошибка проектирования: `wait_for_trigger` на `from: on, to: off` + следующее условие `presence == on` требует presence одновременно `off` и `on`. Проверять выполнимость цепочки **до** заливки. ⚠️ Семантика: `wait.completed` = `true` при срабатывании, `false` при таймауте → «дождаться ухода» = `{{ wait.trigger is none }}`, «дождаться возврата» = `{{ wait.completed }}` | | 36 | 🔴 **`logbook` без поля `message` не различает ветки `choose`** | Версию по `presence` и по `lux` не отличить. Читать с `message`: `jq -r '.[] \| "\(.when) \(.state) \(.message // "")"'` → видно `triggered by state of ` (какая именно ветка). Диагностика ложных включений: сопоставить три ряда `history/period` (`presence` + `illuminance` + `light`) с логбуком обеих автоматизаций в одном окне |