Hero Image

Автополив из бочки: где кончается простота «если — то»

Нам регулярно говорят: «у вас всё переусложнено. В нормальных системах пишешь "если датчик — то клапан", и всё работает». И отчасти это правда: на простейших задачах событийная запись действительно короче и понятнее нашей. Проблема в том, что «работает» здесь — слово с подвохом. Чтобы его разобрать, возьмём реальную автоматизацию средней сложности — автополив из бочки — и пройдём три пути: как её пишем мы, как её соберёт обычный человек в Home Assistant, и во что этот «простой» вариант превращается, когда начинает сталкиваться с реальной жизнью.

HA — мощная и универсальная платформа, и всё описанное ниже в ней в принципе можно сделать корректно. Речь про ментальную модель, на которую платформа наталкивает человека по умолчанию.

Задача словами

Утром, в промежутке с 7 до 9, можем начать полив грядок — если в этот момент вода в бочке достаточно тёплая (выше 20°C) и почва достаточно сухая (влажность ниже 30%). Начавшись, полив длится до двух часов или до тех пор, пока влажность почвы не достигла 40% — что произойдёт раньше. Ночью, с часа до трёх, доливаем бочку из водопровода, если она пустая. Как только бочка наполнилась или ночное окно закончилось — подпитку прекращаем.

Ключевое уточнение: «с 7 до 9» — это окно, в которое полив может стартовать. Если температура дошла до нужной только в 8:55 — полив начнётся и отработает положенные два часа (или до 40% влажности) уже после 9:00. Это важно, потому что дальше мы увидим, как эта деталь влияет на реализацию.

Железо: клапан полива, клапан подпитки, термометр воды в бочке, датчик влажности почвы, контактный датчик уровня в бочке (замкнут — воды нет, надо доливать).

Часть 1. Как это описывается в ПушОк

Начнём с нашего варианта — чтобы потом было с чем сравнивать. Подход ПушОк строится на состояниях, процессах, триггерах и правилах. Правила описывают не «что сделать», а «как должно быть».

Состояния:
  ПодпиткаКран     [Клапан подпитки.Состояние]
  ПоливКран        [Клапан полива.Состояние]
  БочкаУровень     [Датчик уровня.Состояние]
  ТемператураБочка [Термометр бочки.Температура]
  ВлажностьПочва   [Датчик почвы.Влажность]

Процессы:
  ОкноПолива
  ОкноЗалива
  Полив

Триггеры:
  Утро  [07:00]
  Ночь  [01:00]

Правила:
  1. Запустить ОкноПолива на 2ч  если  trigger = Утро
  2. Запустить Полив на 2ч,
     Остановить ОкноПолива,
     если  ОкноПолива И (ТемператураБочка > 20) И (ВлажностьПочва < 30)
  3. Остановить Полив  если  ВлажностьПочва > 40
  4. Запустить ОкноЗалива на 2ч  если  trigger = Ночь
  5. ПодпиткаКран := ОкноЗалива И (БочкаУровень = пусто)
  6. ПоливКран := Полив

Шесть правил на одном экране. Прочитаем как связный текст:

  • (1) В 7 утра открывается окно полива — на 2 часа.
  • (2) Полив запускается внутри окна, если вода тёплая и земля сухая; и в тот же момент окно гасится.
  • (3) Полив останавливается, как только влажность достигла 40%.
  • (4) В 1 ночи открывается окно залива — на 2 часа.
  • (5) Кран подпитки открыт тогда и только тогда, когда мы внутри ночного окна и бочка пустая. Не «открыть при таком-то событии» — а «кран равен этому выражению».
  • (6) Кран полива открыт тогда и только тогда, когда активен процесс Полив.

Два последних правила — ключевые. Они описывают желаемое состояние крана как функцию от других состояний. Любое изменение любого из участников немедленно пересчитывает результат и приводит кран в нужное положение.

На правиле (2) стоит остановиться отдельно — там есть тонкость, важная для понимания модели. Правила в ПушОк перепроверяются при каждом изменении любого из участников. А действие «Запустить процесс на 2 часа» при каждой перепроверке с истинным условием продлевает процесс до 2 часов с текущего момента — не «запускает один раз и забывает», а «держит активным минимум ещё 2 часа, пока условие истинно». Если бы в правиле (2) мы оставили только запуск Полива, происходило бы следующее: температура выше 20, влажность ниже 30, условие истинно → Полив продлевается на 2 часа. Через минуту влажность чуть изменилась (например, с 28 на 27) → перепроверка → условие всё ещё истинно → Полив снова продлён на 2 часа с этого момента. И так пока условие не станет ложным. В итоге полив мог бы идти дольше 2 часов.

Решение — погасить ОкноПолива в том же действии, что запустили Полив. Теперь на следующей перепроверке ОкноПолива уже ложно, и продления не случится. Полив доживёт свои 2 часа от момента старта, что и требовалось. Плюс бонус к надёжности: если Полив досрочно остановит правило (3) по влажности — ОкноПолива уже погашено, повторный запуск в то же утро не случится.

Это один из типичных приёмов в модели состояний: флаг, разрешивший действие, гасится действием, которое он разрешил. Логика «возможность использована — её больше нет».

Упрёк, которого мы ждём на этом месте: «шесть правил вместо двух понятных "если — то"? Зачем?». Чтобы ответить, давайте посмотрим на те самые «два понятных».

Часть 2. Первый заход: как соберёт обычный человек в HA

Он открывает Automations, нажимает «+», и заполняет три раздела: когда сработать, если выполняется условие, что сделать. Думает напрямую — что включать, что выключать. Получается четыре автоматизации:

# Название Когда (Trigger) Если (Condition) То (Action)
1 Полив включить утром время 07:00 темп. бочки выше 20 И влажность почвы ниже 30 включить кран полива
2 Полив выключить время 09:00 или влажность почвы стала выше 40 выключить кран полива
3 Подпитка включить ночью время 01:00 датчик уровня замкнут (бочка пустая) включить кран подпитки
4 Подпитка выключить время 03:00 или датчик уровня разомкнут (бочка наполнилась) выключить кран подпитки

Четыре строки. Читаются как обычный русский текст: «в семь утра включить, если тепло и сухо; в девять выключить, или раньше — если сыро». Эстетически безупречно. Если полив стартовал ровно в 7:00 и за два часа почва дошла до 40% — действительно всё работает.

Разница в компактности с ПушОк-вариантом — очевидная и в пользу HA. На первый взгляд.

Где это ломается

Первое утро проходит гладко. Второе — тоже. Дальше начинается жизнь.

Сценарий 1. Прогрев запоздал. В 7:00 вода в бочке — 19°C. Условие первой автоматизации не выполнено, полив не запустился. В 7:30 солнце прогрело воду до 22°C, земля по-прежнему сухая. Полива не будет. Потому что триггер стоит на «в 7:00», и после этого момента никаких триггеров больше нет — изменение температуры не приводит к повторной проверке условия. Окно полива потеряно до завтра.

По постановке задачи мы хотели другого: окно с 7 до 9 — это окно, в котором полив может стартовать, а не только один момент в 7:00. Человек обнаруживает эту разницу не сразу — по горячим следам он думает «ну, сегодня было прохладно, завтра заработает». Через неделю таких «прохладных» утр становится понятно, что автоматизация прозевала пол-сезона.

«Ну так добавь триггер на температуру», — скажет внимательный читатель. Правильно. Но в первой итерации у пользователя его нет. Он появится после первого провала.

Сценарий 2. Полив оборвался на середине. С этим триггером в 9:00 другая проблема. Допустим, температура сложилась только к 8:30 — полив начался. По постановке задачи он должен работать два часа или до 40% влажности. Но автоматизация 2 просто вырубит его в 9:00 — через полчаса после старта. Полтора часа полива украдены.

Если пользователь заметит — перепишет «время 09:00» на что-то вроде «через 2 часа после старта». Это уже не триггер по времени в календарном смысле, а таймер. Таймер в HA есть, но это отдельная сущность, которую надо явно создать в конфигурации, дать ей имя, а потом запускать и останавливать из автоматизаций. Плюс про него надо знать, что он по умолчанию не переживает перезагрузки системы — для этого в его настройках нужно включить галочку Restore. То есть простой триггер «в 9:00» превращается в целое маленькое хозяйство. У нас таймерам посвящена отдельная статья — в ней разбирается, почему «запустить процесс на N секунд» надёжнее и удобнее, чем «включить в момент X, выключить в момент X+N».

Сценарий 3. Рестарт в неудобный момент. Обновление прилетело в 8:57, рестарт занял пять минут. Даже если мы уже знаем про таймеры и поставили Restore, рестарт всё равно ломает другой случай: предположим, полив не запущен, пользователь живёт с первой версией автоматизации, и триггер «в 9:00» пришёл, когда HA был выключен. Пропустили его — пропустили и остановку. Если полив в это время шёл, кран останется открытым. Второй шанс закрыть — триггер по влажности выше 40, но если почва уже напиталась и датчик залип (у гигрометров есть такое свойство при насыщении), этот триггер может не сработать. До 9:00 следующего дня.

Сценарий 4. Влажность уже была высокой. Ночью прошёл дождь, утром почва сырая — датчик показывает 45%. В 7:00 условие «ниже 30» не выполнено, всё правильно, полива нет. Теперь другой день: датчик показывает 28% (поверхность подсохла, в глубине ещё сыро). Условие выполнено, полив пошёл. Через десять минут датчик устаканился и показал 42%. Здесь мы рассчитываем, что вторая автоматизация сработает по триггеру «влажность стала выше 40». Но этот триггер ловит момент пересечения порога снизу вверх. Если последовательность показаний была 28 → 42, пересечение есть, поймали. А если была 28 → 45 → 43 → 45 → 41 (обычные флуктуации сырого датчика) — пересечения 40 снизу вверх нет ни одного. Кран будет лить до 9:00.

Сценарий 5. Ручное вмешательство. В полдень кто-то из домочадцев открыл приложение HA, увидел «кран полива выключен», решил «полью-ка я вручную». Включил. Забыл. Автоматизации 1 и 2 сегодня уже отработали — они срабатывают по своим триггерам, а не «следят за тем, каким должен быть кран». До завтрашних 9:00 никто его не выключит.

Сценарий 6. Ночная подпитка не дотянула. В 1:00 бочка пустая — автоматизация 3 открыла подпитку. В 1:30 бочка наполнилась — автоматизация 4 закрыла. В 2:15 где-то подтекает, поплавок снова опустился. Автоматизации для этого случая нет: «включить в час ночи» уже отработало сегодня и сработает опять только завтра. Бочка простоит пустой до утра.

Важно понимать: ни один из этих сценариев не теоретический. Это бытовые ситуации, которые случаются за сезон. Между «я собрал автоматизацию» и «она попала в один из случаев» проходит несколько недель. К моменту, когда случается, никто уже не связывает «огород залит» с рестартом в 8:57.

Часть 3. Надёжная автоматизация в HA: что получается после всех правок

После каждого неприятного сюрприза человек возвращается в автоматизации и добавляет ещё одно правило, ещё один триггер, ещё одно условие. Через сезон конструкция выглядит так:

# Название Когда (Trigger) Если (Condition) То (Action)
1 Запуск окна полива время 07:00 запустить таймер ОкноПолива на 2 часа
2 Запуск полива температура бочки стала выше 20 или влажность стала ниже 30 или запущен таймер ОкноПолива таймер ОкноПолива активен И темп. бочки выше 20 И влажность ниже 30 И таймер Полив неактивен запустить таймер Полив на 2 часа
3 Остановка полива по влажности влажность стала выше 40 таймер Полив активен отменить таймер Полив
4 Кран полива — открыть таймер Полив запущен включить кран полива
5 Кран полива — закрыть таймер Полив завершился или таймер Полив отменён или кран полива самопроизвольно включился таймер Полив неактивен выключить кран полива
6 Запуск окна залива время 01:00 запустить таймер ОкноЗалива на 2 часа
7 Подпитка — открыть таймер ОкноЗалива запущен или датчик уровня замкнулся (стало пусто) или кран подпитки самопроизвольно выключился таймер ОкноЗалива активен И датчик уровня замкнут включить кран подпитки
8 Подпитка — закрыть таймер ОкноЗалива завершился или таймер ОкноЗалива отменён или датчик уровня разомкнулся (стало полно) или кран подпитки самопроизвольно включился (таймер ОкноЗалива неактивен) или (датчик уровня разомкнут) выключить кран подпитки

Восемь автоматизаций. Столбец Когда у половины из них — длинный список альтернатив, столбец Если — почти полное повторение логики из Когда, но сформулированное иначе. Это не ошибка пользователя и не плохой стиль — это архитектура платформы: триггеры ловят моменты (в том числе «только что пересекли порог», «только что запустили таймер»), а условия проверяют текущее положение дел. Одно без другого работать не будет.

Пройдёмся по неочевидным местам этой таблицы — чтобы было видно, какой запас знаний понадобился пользователю, чтобы до неё дойти.

Таймеры вместо «время 09:00». Триггер «время такое-то» не переживает рестарта — момент просто проходит мимо выключенной системы. Таймер, в отличие от триггера по времени, после рестарта продолжает с того места, где остановился — если в настройках самого таймера проставить галочку Restore. Про эту галочку надо знать заранее: по умолчанию она выключена.

timer.finished и timer.cancelled — разные события. Таймер может закончиться двумя способами: естественно (истекло время) или принудительно (кто-то вызвал timer.cancel). В HA это два разных события. Если в триггере автоматизации «Кран полива — закрыть» указать только timer.finished — ручная остановка полива через «Остановка полива по влажности» не закроет кран. Нужно перечислять оба события.

«Кран включился самопроизвольно» — отдельный триггер. Это ответ на сценарий 5. Чтобы автоматизация отыгрывала ручное вмешательство, нужно явно подписаться на изменение состояния самого крана и при сравнении с ожидаемым состоянием вернуть его на место. Без этого триггера кран, открытый руками, никакая автоматизация обратно не закроет.

Сценарий 6 (подпитка во время ночного окна) решается не добавлением новой автоматизации, а переделкой логики. В первом заходе было: «в 1:00 включить, если пусто; в 3:00 выключить». В надёжном: «включить, когда внутри окна И пусто; выключить, когда вне окна ИЛИ полно». Это уже не пара команд на границах интервала, а поддержание инварианта внутри интервала. По сути, пользователь приходит здесь к той же идее, что у нас выражена правилом ПодпиткаКран := ОкноЗалива И (БочкаУровень = пусто) — но выражает её двумя разнесёнными автоматизациями (7 и 8).

Автоматизации 7 и 8 описывают один кран. Чтобы понять, в каком состоянии подпитка должна быть в конкретный момент, читателю этой конфигурации нужно открыть обе карточки и сложить их в голове. И ещё раз перепроверить, что они не противоречат друг другу на границах: нет ли случая, когда и «открыть» и «закрыть» одновременно истинны? Если есть — чья возьмёт, зависит от того, какая сработала последней, что зависит от порядка событий.

Часть 4. А что если команда не дошла?

До этого момента мы по умолчанию предполагали, что отправленная команда крану — доходит. В Zigbee это предположение не всегда верно. Краны бывают на батарейках, просыпаются раз в N секунд; ретрансляторы теряют пакеты; роутеры перезагружаются. Команда включи кран полива может уйти — и остаться без подтверждения.

Это отдельный класс ненадёжности, который не закрывается ни одним из шести сценариев выше. Те сценарии — про «автоматизация не решила что надо». А этот — про «решила, но не дошло до исполнителя».

Как это решается в ПушОк

Тот же приём, который мы использовали в статье про АкваСтоп для гарантии закрытия крана при протечке — только здесь сигнал для проверки более общий, «команда доставлена/не доставлена».

Добавляем один процесс и одно правило:

Процессы:
  ... (прежние)
  КонтрольКрана

Правила:
  ... (прежние)
  7. Запустить КонтрольКрана на 30 секунд,
     если НЕ ДОШЛО(ПоливКран)

Всё. Разберём, как это работает.

Правило 6 ПоливКран := Полив при каждой перепроверке формирует zigbee-команду устройству — даже если вычисленное значение не изменилось со времени прошлой проверки. Это добавляет немного лишнего трафика, но даёт то, ради чего всё затевалось: каждая перепроверка правила — это ещё одна попытка доставить команду.

Функция ДОШЛО(ПоливКран) возвращает истину, если последняя отправленная команда крану была подтверждена устройством, и ложь — если нет.

Правило 7: пока команда не подтверждена, процесс КонтрольКрана активен и продлевается на 30 секунд при каждой перепроверке. Естественное завершение процесса, как и любое изменение его состояния, автоматически триггерит перепроверку правил, его использующих. В этот момент:

  • если подтверждение за эти 30 секунд пришло — на перепроверке правила 7 условие НЕ ДОШЛО стало ложным, действие не выполняется, процесс остаётся завершённым;
  • если подтверждения нет — условие всё ещё истинно, процесс продлевается ещё на 30 секунд, а попутно правило 6 в этой же перепроверке снова отправляет команду крану.

Получается аккуратная петля: пока команда не доходит, каждые 30 секунд отправляется новая попытка. Как только дошла — петля сама тихо прекращается на ближайшем завершении процесса.

Обратите внимание: мы не писали «повтори команду 10 раз с интервалом 3 секунды». Мы не писали счётчик попыток, не писали цикл, не писали условие выхода. Мы описали состояние, которое система должна поддерживать: «если команда не дошла — процесс контроля активен». Всё остальное следует из механики перепроверки правил.

Как это решается в HA

А вот здесь первый заход пользователя HA уже не дотягивает. Для проверки того, что команда дошла до устройства, в HA нужно:

  1. Отправить команду (switch.turn_on);
  2. Подождать какое-то время (delay);
  3. Проверить, что state switch'а стал ожидаемым;
  4. Если нет — повторить.

Через UI-автоматизации это не выражается. Нужен script — отдельная сущность в YAML-конфиге, с конструкцией repeat: until:. Примерно так:

script:
  set_poliv_kran:
    sequence:
      - repeat:
          until:
            - condition: template
              value_template: >
                {{ states('switch.poliv_kran') == target }}
          sequence:
            - service: "switch.turn_{{ 'on' if target == 'on' else 'off' }}"
              target:
                entity_id: switch.poliv_kran
            - delay: "00:00:30"

Плюс все восемь автоматизаций из таблицы части 3, которые раньше вызывали switch.turn_on и switch.turn_off напрямую, теперь должны вызывать этот скрипт с параметром. Плюс нужно вручную продумать: выходить из цикла по числу попыток или по таймауту? Отправлять уведомление или нет? А что если в середине цикла автоматизация получила новую команду (например, «закрой кран», пока скрипт ещё пытается открыть) — что тогда?

Вопросов много, и ни один из них не решается «одним кликом» в интерфейсе. Это уже полноценное программирование.

При этом ни один из перечисленных вопросов не возник в нашем варианте. «Выходить по числу попыток или по таймауту» — не возник, потому что петля контролируется не счётчиком, а условием «дошло/не дошло». «Отправлять уведомление» — решается галочкой в настройках процесса. «Что если команда поменялась в середине» — решается автоматически, потому что правило 6 пересчитывает актуальное значение на каждой перепроверке; процесс контроля просто продолжает проверять «дошла ли текущая команда», независимо от того, сколько раз она поменялась.

Это и есть главная выгода декларативной модели: мы не описывали алгоритм повтора — мы описали инвариант «процесс активен, пока не подтверждено». Алгоритм получился сам.

Часть 5. Автоматизация как единица

Есть ещё одно различие между нашей моделью и HA, которое не видно в правилах, но сильно сказывается на жизни. В ПушОк автоматизация — это семантическая единица: совокупность состояний, процессов, триггеров и правил, живущих вместе. Её можно активировать и деактивировать целиком. Её можно клонировать целиком. Из неё можно сделать шаблон — и применить к новому набору устройств.

В HA аналогичной единицы нет. То, что у нас является одной автоматизацией, в HA — это набор из 8 независимых automation плюс 3 helper-таймера. Каждая из них сама по себе не знает, что принадлежит к одной логической задаче. Это не мелочь, а принципиально другая архитектура.

Рассмотрим три обычных жизненных ситуации, в которых это различие становится болезненным.

Ситуация 1. Сезонность. Полив нужен с мая по сентябрь. Зимой он не просто не нужен — он вреден (не дай бог клапан откроется в мороз). У нас: один переключатель, автоматизация выключается целиком, все её правила, процессы и триггеры перестают работать. Весной — обратно одним переключателем. В HA: нужно найти все восемь автоматизаций и три таймера, и отключить каждую отдельно. Забыл отключить одну — где-то щёлкнет клапан зимой. Обходные пути в сообществе HA обсуждаются уже годы: boolean-helper vacation_mode плюс condition vacation_mode = off в каждой automation, или мастер-automation, которая переключает остальные по имени, или labels с массовым homeassistant.turn_off. Всё это — слой, который пользователь должен спроектировать сам, потому что встроенного механизма «выключить эту группу» в HA нет.

Ситуация 2. Одинаковое решение в нескольких местах. У вас две теплицы с одинаковым набором железа. У нас: клонируешь автоматизацию одним кликом, в клоне перепривязываешь состояния на датчики и краны второй теплицы — готово. Минуты три. В HA: клонируем первую automation (есть такая функция), потом вторую, потом третью... и так восемь. Создаём вручную три новых таймера для второй теплицы. Затем в каждой из восьми копий меняем все entity_id. Если в автоматизации упоминаются вспомогательные helpers — их тоже надо продублировать. Если где-то что-то пропустили — вторая теплица ведёт себя как первая, и найти ошибку будет непросто.

Ситуация 3. Готовые комплекты с преднастроенной автоматизацией. Мы продаём комплект автополива POK403 — к нему прилагается шаблонная автоматизация, которую пользователь применяет к своему железу и сразу получает рабочее решение. В HA для похожей задачи есть Blueprints — но они рассчитаны на одну automation. Для группы автоматизаций + taймеров, покрывающей целый сценарий, Blueprints не работают. Альтернатива — Packages: пользователь скачивает YAML-файл и кладёт его в конфигурацию. Это работает, но уже не является UI-сценарием: человек, который хочет «всё через интерфейс», упирается в необходимость редактировать YAML.

Все три ситуации — следствие одного и того же различия. У нас есть сущность, которой можно оперировать как целым, и это открывает возможности, которые в событийной модели без встроенной группировки либо невозможны, либо требуют обходных путей.

Что в итоге получилось

Три разные автоматизации для одной и той же задачи:

Сущностей Читается Выдерживает шесть сценариев Контроль доставки команд
HA, первый заход 4 автоматизации как обычный русский текст нет нет
Надёжный HA 8 автоматизаций, 3 таймера-helper'а по одной карточке, сводя вместе да, если ничего не забыть требует скрипта с repeat: until: вдобавок
ПушОк 7 правил, 4 процесса семь строк на одном экране да, по построению одно правило плюс один процесс

Критический упрёк «у вас всё сложно» возникает из сравнения ПушОк с первым вариантом — с четвёркой первого захода. И если бы он действительно работал надёжно, упрёк был бы справедлив. Но надёжно он работает только до первого провалившегося сценария. После этого либо он превращается во второй вариант, либо остаётся как есть — с залитыми огородами и кранами, которые кто-то забыл закрыть.

Сравнивать наш шестистрочный вариант надо не с четвёркой первого захода, а с надёжной восьмёркой. И вот на этом сравнении внезапно выясняется, что:

  • количество сущностей у ПушОк меньше, а не больше;
  • логика одного физического крана в ПушОк описана одной строкой, в HA — двумя автоматизациями;
  • перепроверка условий при любом изменении участников в ПушОк встроена, в HA — достигается ручным перечислением триггеров;
  • шесть описанных сценариев в ПушОк обрабатываются автоматически, в HA — каждый отдельно пришлось вписать;
  • автоматизация в ПушОк — единое целое, её можно включить, выключить, клонировать одним действием, в HA это либо невозможно, либо требует самодельных обходных путей.

Переход к модели состояний требует некоторого разворота в голове. Но сам он не усложняет, а наоборот: сразу ведёт к решению, которое выдерживает обычную жизнь с рестартами, залипшими датчиками и людьми, которые иногда что-то переключают руками. И, как мы видели в Части 4, достаточно описать правильный инвариант — а петля повторов, контроль доставки и всё остальное получатся сами.

Для автоматизации полива из бочки у нас есть готовый комплект со всеми нужными устройствами. А подход, который мы здесь сравнивали, мы продолжим разбирать на новых примерах в следующих статьях. Все в наших руках!