Кухня научилась отличать человека от растений
Обещанная история про автоматизацию наконец готова. На кухне — три умные лампы, датчик движения, физическая кнопка на стене и Home Assistant, которому нужно было развести между собой два совершенно разных запроса: «человек зашёл» и «растениям пора светить».
Первая версия автоматики называлась просто «Kitchen Controller» и прожила недолго — она путала одно с другим: то гасила свет растениям, потому что никто не двигался, то держала яркий холодный свет ради человека, который зашёл на десять секунд налить воды. Переписанная версия развела логику на отдельные сценарии с чётким приоритетом.
Главный — ручной режим, в него автоматика не лезет вообще. Если он не включён, дальше решает, кто сейчас «хозяин» освещения: если на кухне активен режим для растений — движение человека игнорируется полностью, досветка не должна прерываться каждый раз, когда кто-то прошёл мимо. Если нет — обычная логика присутствия: одна лампа тёплого света на минимальной яркости для случайного захода, две — если движение повторяется достаточно часто, чтобы считать кухню «занятой».
Досветка для растений включается отдельным условием — только когда темно, только в дневное окно (примерно с шести утра до одиннадцати вечера, чтобы не мешать ночному циклу), и только если растения явно не поставлены на паузу. Тогда врубаются все три лампы разом, на максимум яркости и на холодный дневной свет — визуально это совсем не то же самое, что тёплый вечерний свет для человека, спутать одно с другим больше нельзя.
Самая приятная деталь — как гаснет свет. Не разом, а по одной лампе, растягивая на несколько минут, и в любой момент, если кто-то вернулся или тронул кнопку — гашение прерывается на середине. Мелочь, но именно из-за таких мелочей автоматика в итоге начинает ощущаться как что-то разумное, а не как реле по таймеру.
Ушло на это несколько итераций и забытых edge-case'ов. Кота автоматика по-прежнему не видит — но это уже другая история.
Если хотите повторить
Ниже — реальные automation-скрипты Home Assistant, только с обезличенными ID устройств (замените на свои: три лампы, датчик движения, при желании — физическая Zigbee-кнопка с одиночным/двойным/долгим кликом). Каждый скрипт свёрнут — разворачивайте по одному.
Что завести заранее
Кроме самих ламп и датчика движения, автоматике нужны вспомогательные объекты (создаются в Home Assistant как Helpers, вручную или через configuration.yaml):
input_select.kitchen_mode— варианты:off,auto,manual,plantsinput_boolean.kitchen_automation_enabled— общий рубильник автоматикиinput_boolean.kitchen_plants_enabled— отдельный тумблер именно для режима растенийinput_boolean.kitchen_button_direction_up— направление следующего цикла яркости по кнопкеinput_datetime.kitchen_last_motion,input_datetime.kitchen_last_button,input_datetime.kitchen_soft_window_startedinput_number.kitchen_light_countcounter.kitchen_soft_motion_countertimer.kitchen_timer- свои скрипты
script.kitchen_apply,script.kitchen_idle_timer,script.kitchen_button,script.kitchen_motion,script.kitchen_plants_tick,script.kitchen_wind_down,script.kitchen_plants_resume— сюда их роутит главный скрипт ниже
Kitchen — главный роутер
alias: Kitchen
description: Тонкий роутер кухни. Старый Kitchen Controller должен быть выключен.
triggers:
- trigger: homeassistant
event: start
id: startup
- trigger: state
entity_id: binary_sensor.motion_sensor
from: "off"
to: "on"
id: motion
- trigger: state
entity_id: event.kitchen_button_click
id: button_single
not_from:
- unavailable
not_to:
- unavailable
- trigger: state
entity_id: event.kitchen_button_double_click
id: button_double
not_from:
- unavailable
not_to:
- unavailable
- trigger: state
entity_id: event.kitchen_button_long_press
id: button_long
not_from:
- unavailable
not_to:
- unavailable
- trigger: event
event_type: timer.finished
event_data:
entity_id: timer.kitchen_timer
id: timer_finished
- trigger: state
entity_id: light.kitchen_hub_led
id: dark_changed
- trigger: state
entity_id: sun.sun
to:
- above_horizon
- below_horizon
id: dark_changed
- trigger: state
entity_id: input_boolean.kitchen_plants_enabled
id: dark_changed
- trigger: time
at: "06:00:00"
id: dark_changed
- trigger: time
at: "23:00:00"
id: dark_changed
actions:
- choose:
- conditions:
- condition: trigger
id: motion
sequence:
- action: script.kitchen_motion
- conditions:
- condition: trigger
id: button_single
sequence:
- action: script.kitchen_button
data:
kind: single
- conditions:
- condition: trigger
id: button_double
sequence:
- action: script.kitchen_button
data:
kind: double
- conditions:
- condition: trigger
id: button_long
sequence:
- action: script.kitchen_button
data:
kind: long
- conditions:
- condition: trigger
id: timer_finished
sequence:
- action: script.turn_on
target:
entity_id: script.kitchen_wind_down
- conditions:
- condition: trigger
id:
- dark_changed
- startup
sequence:
- action: script.kitchen_plants_tick
- action: logbook.log
data:
name: Kitchen
message: >
dark={{ is_state('light.kitchen_hub_led', 'on') or
(states('light.kitchen_hub_led') in ['unavailable',
'unknown']
and is_state('sun.sun', 'below_horizon')) }}
hub={{ states('light.kitchen_hub_led') }} sun={{
states('sun.sun') }} mode={{ states('input_select.kitchen_mode')
}}
mode: queued
max: 15Kitchen Motion — реакция на движение
alias: Kitchen Motion
mode: queued
max: 10
sequence:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.kitchen_last_motion
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
- variables:
hub: "{{ states('light.kitchen_hub_led') }}"
hub_ok: "{{ hub not in ['unavailable', 'unknown'] }}"
is_dark: "{{ (hub == 'on') if hub_ok else is_state('sun.sun', 'below_horizon') }}"
auto_on: "{{ is_state('input_boolean.kitchen_automation_enabled', 'on') }}"
mode_now: "{{ states('input_select.kitchen_mode') }}"
any_on: |
{{ is_state('light.yeelight_kitchen_1', 'on')
or is_state('light.yeelight_kitchen_2', 'on')
or is_state('light.yeelight_kitchen_3', 'on') }}
window_ok: >
{% set started = states('input_datetime.kitchen_soft_window_started') %}
{% if started in ['unknown', 'unavailable', 'none', ''] %}
false
{% else %}
{{ (as_timestamp(now()) - as_timestamp(started)) < 300 }}
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ not auto_on }}"
sequence:
- if:
- condition: template
value_template: "{{ any_on }}"
then:
- action: script.kitchen_idle_timer
data:
minutes: 60
- stop: automation off, only failsafe
- if:
- condition: template
value_template: "{{ mode_now == 'plants' }}"
then:
- stop: plants ignore people
- action: script.turn_off
target:
entity_id: script.kitchen_wind_down
- choose:
- conditions:
- condition: template
value_template: "{{ not window_ok }}"
sequence:
- action: counter.reset
target:
entity_id: counter.kitchen_soft_motion_counter
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.kitchen_soft_window_started
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
- action: counter.increment
target:
entity_id: counter.kitchen_soft_motion_counter
- variables:
busy: "{{ (states('counter.kitchen_soft_motion_counter') | int(0)) >= 3 }}"
- choose:
- conditions:
- condition: template
value_template: "{{ mode_now == 'manual' }}"
sequence:
- action: script.kitchen_idle_timer
data:
minutes: 10
- conditions:
- condition: template
value_template: "{{ not any_on }}"
sequence:
- if:
- condition: template
value_template: "{{ is_dark }}"
then:
- action: input_select.select_option
target:
entity_id: input_select.kitchen_mode
data:
option: auto
- action: script.kitchen_apply
data:
count: "{{ 2 if busy else 1 }}"
brightness_pct: 30
kelvin: 2700
- action: script.kitchen_idle_timer
data:
minutes: 5
default:
- action: input_select.select_option
target:
entity_id: input_select.kitchen_mode
data:
option: auto
- action: script.kitchen_apply
data:
count: "{{ 2 if busy else 1 }}"
brightness_pct: 30
kelvin: 2700
- action: script.kitchen_idle_timer
data:
minutes: 5
description: ""Kitchen Plants Tick — досветка растений
alias: Kitchen Plants Tick
mode: queued
max: 5
sequence:
- variables:
hub: "{{ states('light.kitchen_hub_led') }}"
hub_ok: "{{ hub not in ['unavailable', 'unknown'] }}"
is_dark: "{{ (hub == 'on') if hub_ok else is_state('sun.sun', 'below_horizon') }}"
time_ok: >-
{{ now().strftime('%H:%M') >= '06:00' and now().strftime('%H:%M') <
'23:00' }}
paused: "{{ is_state('script.kitchen_plants_resume', 'on') }}"
auto_on: "{{ is_state('input_boolean.kitchen_automation_enabled', 'on') }}"
plants_on: "{{ is_state('input_boolean.kitchen_plants_enabled', 'on') }}"
eligible: "{{ auto_on and plants_on and is_dark and time_ok and not paused }}"
mode_now: "{{ states('input_select.kitchen_mode') }}"
motion_recent: >
{% set last = states('input_datetime.kitchen_last_motion') %} {% if last
in ['unknown', 'unavailable', 'none', ''] %}
false
{% else %}
{{ (as_timestamp(now()) - as_timestamp(last)) < 300 }}
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ eligible }}"
sequence:
- action: input_select.select_option
target:
entity_id: input_select.kitchen_mode
data:
option: plants
- action: script.kitchen_apply
data:
count: 3
brightness_pct: 100
kelvin: 4000
- action: timer.cancel
target:
entity_id: timer.kitchen_timer
- conditions:
- condition: template
value_template: "{{ mode_now == 'plants' and not eligible }}"
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ not time_ok }}"
sequence:
- choose:
- conditions:
- condition: template
value_template: >-
{{ is_state('binary_sensor.motion_sensor', 'on')
or motion_recent }}
sequence:
- action: input_select.select_option
target:
entity_id: input_select.kitchen_mode
data:
option: auto
- action: script.kitchen_apply
data:
count: 1
brightness_pct: 30
kelvin: 2700
- action: script.kitchen_idle_timer
data:
minutes: 5
default:
- action: script.turn_on
target:
entity_id: script.kitchen_wind_down
default:
- action: script.kitchen_apply
data:
count: 0
brightness_pct: 30
kelvin: 2700
- action: timer.cancel
target:
entity_id: timer.kitchen_timer
- action: input_select.select_option
target:
entity_id: input_select.kitchen_mode
data:
option: "off"
description: ""Kitchen Wind Down — плавное гашение
alias: Kitchen Wind Down
mode: restart
sequence:
- variables:
started: "{{ as_timestamp(now()) }}"
on_lamps: >
{{ expand(
'light.yeelight_kitchen_1',
'light.yeelight_kitchen_2',
'light.yeelight_kitchen_3'
) | selectattr('state', 'eq', 'on') | map(attribute='entity_id') | list
}}
"n": "{{ on_lamps | count }}"
step: "{{ [((300 / [n | int, 1] | max) | int), 30] | max }}"
- choose:
- conditions:
- condition: template
value_template: "{{ n | int == 0 }}"
sequence:
- action: timer.cancel
target:
entity_id: timer.kitchen_timer
- action: input_select.select_option
target:
entity_id: input_select.kitchen_mode
data:
option: "off"
- stop: already off
- repeat:
for_each: "{{ on_lamps[::-1] }}"
sequence:
- delay:
seconds: "{{ step | int }}"
- if:
- condition: template
value_template: |
{{ is_state('binary_sensor.motion_sensor', 'on')
or (as_timestamp(states('input_datetime.kitchen_last_button')) | float(0))
> (started | float) }}
then:
- stop: interrupted
- action: light.turn_off
target:
entity_id: "{{ repeat.item }}"
- action: timer.cancel
target:
entity_id: timer.kitchen_timer
- action: input_select.select_option
target:
entity_id: input_select.kitchen_mode
data:
option: "off"
- action: input_number.set_value
target:
entity_id: input_number.kitchen_light_count
data:
value: 0
- action: input_boolean.turn_on
target:
entity_id: input_boolean.kitchen_button_direction_up
description: ""Подводные камни
kitchen_apply(count, brightness_pct, kelvin)иkitchen_idle_timer(minutes)— не сами automation, а отдельные вспомогательные скрипты, здесь не приведены целиком. Их поведение видно из того, как их вызывают:kitchen_applyвключает нужное число ламп с заданной яркостью и температурой,kitchen_idle_timerпросто заводит (или обновляет) таймер на N минут.- Один и тот же id
dark_changedвисит сразу на нескольких разных триггерах (смена состояния лампы-индикатора хаба, восход/закат, ручной тумблер, два фиксированных времени) — это не дублирование, а осознанное решение: любое из этих событий должно просто пересчитать «темно сейчас или нет», а не запускать разную логику под каждое. is_darkсначала пытается определить темноту по состоянию лампы-индикатора хаба, и только если та недоступна — падает на восход/закат. Без этого резерва вся логика зависает, если сам хаб временно отвалился.mode: queuedс ограничением по параллельным запускам в главном роутере — чтобы быстрые события подряд (например, двойной клик сразу после одиночного) не создавали гонку состояний.- Kitchen Wind Down —
mode: restart, а неqueued: если во время затухания приходит новое движение, скрипт должен начаться заново (по факту — прерваться), а не встать в очередь и доиграть старое гашение поверх нового состояния. - Счётчик «занятости» кухни считает срабатывания датчика движения в скользящем 5-минутном окне, а не одно единственное движение — иначе один случайный проход считался бы «людной кухней».
- Режим
plantsнамеренно игнорирует присутствие человека — без этой развязки досветка растений дёргалась бы каждый раз, когда кто-то просто проходит мимо. - Entity ID здесь обезличены (
light.yeelight_kitchen_1/2/3,event.kitchen_button_*) — замените на настоящие имена ваших устройств в Home Assistant, иначе ничего не сработает.
раньше Админ наблюдает за процессом. Процесс явно опережает по темпу.
новее Мирабилис достиг стадии «это точно комнатное растение?»
Обсуждение
Комментировать могут зарегистрированные читатели.
Войти Регистрация