Home Assistant light automations

Ola Thoresen
11 min readAug 14, 2020

After a brief discussion in the HA-forums earlier this summer I decided to write up some more details about my take on automations in Home Assistant. I have been through many incarnations of the “lights on, lights off”-automations, and I guess these are what quite a lot of people begin with when they start thinking of home automaton. I don’t say that this is the perfect way, or that everyone should follow this approach, or that is is very unique, but I do think it is slightly different from what most people do, so it can at least maybe give you some ideas.

First of all, I have had some very, very complex automations. As described in earlier posts, after things work for a while, you realize that even the most trivial automations can have corner cases and room for improvements. Over time this grows to unmaintanable beasts of conditions and states you need to track. So after I gave up trying to figure out why a certain light did not behave as I expected, I decided to take a step back and try to build something that was much more maintainable and easy to debug. I try to follow “the unix philosophy”, and let all my automations do one thing, and to that thing well.

Some background

In all our rooms we have at least one motion sensor, and every light in the appartment is controlled by a physical “smart” switch or dimmer (Fibaro zwave), which I can operate from Home Assistant. That way al lights can be turned on and off both by the physical switch, and by HA. My goal is to “never” have to use the physical switches. They should only be used as “backup” in case an automation does not work correctly, or in situations that was impossible to predict, so no automation is written for it (yet). Lately, we have been pretty close to this goal. These days we usually only use one physical switch when we go to bed and another when we leave the house. I prefer to just push a button rather than some kind of voice-controlled “Hey Google, good night”, but that is just my choice.
If we don’t push the button, device trackers and motion sensors will still detect that it is night or that everyone is out anyway, but it takes a bit more time before those trigger.

Automations

Ok, so back to the automations. At the heart of every light automation, I have a couple of inputs.

Some input_numbers for brightness of the lights (one for each light)

input_number:
brightness_bathroom:
name: Brightness Bathroom
min: 0
max: 100
step: 1
brightness_kitchen_table:
name: Brightness Kitchen Table
min: 0
max: 100
step: 1
brightness_kitchen_ceiling:
name: Brightness Kitchen ceiling
min: 0
max: 100
step: 1

And some input_datetimes to set the delay before the light is turned off again after the motion sensor has been turned off. Here I use one for each light — or room, depending a bit on the room. In some rooms I want individual timers for each lamp. In other rooms, I want the whole room to go dark.

input_datetime:
lights_off_timer_bathroom:
name: Timer lights off - Bathroom
has_date: false
has_time: true
lights_off_timer_kitchen_table:
name: Timer lights off - Kitchen Table
has_date: false
has_time: true
lights_off_timer_kitchen_ceiling:
name: Timer lights off - Kitchen ceiling
has_date: false
has_time: true

This allows me to easily adjust the brightness and timeouts both from Lovelace and from automations.

Then I have four extremely simple automations for each light/room.

Two “trigger”-automations:

- id: motion_on_bathroom
alias: Motion on - Bathroom
trigger:
- entity_id: binary_sensor.motion_bathroom
platform: state
from: 'off'
to: 'on'
action:
- event: motion_on_bathroom
- id: motion_off_bathroom
alias: Motion off - Bathroom
trigger:
- entity_id: binary_sensor.motion_bathroom
platform: state
from: 'on'
to: 'off'
for: "{{ states('input_datetime.lights_off_timer_bathroom') }}"
action:
- event: motion_off_bathroom

As you can see, the two automations does not do anything but send an event. And they do so unconditionally when motion is detected. The “motion off” automation uses the mentioned “input_datetime” in the trigger to decide when to fire. If I have more than one sensor in a room, I set up both sensors to send two events:

- id: motion_on_kitchen_1
alias: Motion on - Kitchen Sensor 1
trigger:
- entity_id: binary_sensor.motion_kitchen_1
platform: state
from: 'off'
to: 'on'
action:
- event: motion_on_kitchen
- event: motion_on_kitchen_sensor_1
- id: motion_on_kitchen_2
alias: Motion on - Kitchen Sensor 2
trigger:
- entity_id: binary_sensor.motion_kitchen_2
platform: state
from: 'off'
to: 'on'
action:
- event: motion_on_kitchen
- event: motion_on_kitchen_sensor_2

I could use a group, but I might want to know which one was triggered, so I just let them both send the same event “motion_on_kitchen”, and their own indiviual event.

For “motion off” I use a group because I want both sensors to be off before sending the off-event.

- id: motion_off_kitchen
alias: Motion off - kitchen
trigger:
- entity_id: group.kitchen_motion_sensors
platform: state
from: 'on'
to: 'off'
for: "{{ states('input_datetime.lights_off_timer_kitchen_table') }}"
action:
- event: motion_off_kitchen

Again, I could use single automations for each sensor, and have the other one as a condition, but I want to avoid using conditions as much as possible.

- id: motion_off_kitchen_1
alias: Motion off - kitchen 1
trigger:
- entity_id: binary_sensor.motion_kitchen_1
platform: state
from: 'on'
to: 'off'
for: "{{ states('input_datetime.lights_off_timer_kitchen_table') }}"
condition:
- condition: state
entity_id: binary_sensor.motion_kitchen_2
state: 'off'
action:
- event: motion_off_kitchen
- id: motion_off_kitchen_2
alias: Motion off - kitchen 2
trigger:
- entity_id: binary_sensor.motion_kitchen_2
platform: state
from: 'on'
to: 'off'
for: "{{ states('input_datetime.lights_off_timer_kitchen_table') }}"
condition:
- condition: state
entity_id: binary_sensor.motion_kitchen_1
state: 'off'
action:
- event: motion_off_kitchen

Then I have two “hub”-automations that decides which lights are controlled by which sensors:

- id: lights_on_bathroom
alias: Lights On - Bathroom
trigger:
- platform: event
event_type: motion_on_bathroom
- platform: event
event_type: lights_on_bathroom
action:
- event: lights_on
event_data_template:
entity_id: light.dimmer_bathroom
light_source: bathroom
- id: lights_off_bathroom
alias: Lights Off - Bathroom
trigger:
- platform: event
event_type: motion_off_bathroom
- platform: event
event_type: lights_off_bathroom
- platform: event
event_type: lights_off_all
- platform: event
event_type: lights_off_away
- platform: event
event_type: lights_off_night
action:
- event: lights_on
event_data_template:
entity_id: light.dimmer_bathroom
light_source: bathroom

So the two “action” automations are listening for the events from the different “trigger” automations, and will just send another event, including the light they want to turn on or off.

Agian, there are no conditions. If one of the events are received, the automation will run and the event will be sent.

I also listen for a few more events than the “motion_on” and “motion_off”, so other automations can send other events. That make it easier to see what actually triggered the automaton to run, and to be able to easily add and change “trigger” automations without having to modify the “action” automations.

And finally, I have the actual “lights on” and “lights off” automations.

- id: lights_on
alias: Lights On
mode: parallel
trigger:
- platform: event
event_type: lights_on
action:
- choose:
- conditions:
- condition: template
value_template: "{{ trigger.event.data.entity_id[:6] == 'switch' }}"
- condition: template
value_template: "{{ states(trigger.event.data.entity_id) == 'off' }}"
sequence:
- service: switch.turn_on
data_template:
entity_id: "{{ trigger.event.data.entity_id }}"
- conditions:
- condition: template
value_template: "{{ trigger.event.data.entity_id[:5] == 'light' }}"
- condition: or
conditions:
- condition: template
value_template: "{{ states(trigger.event.data.entity_id) == 'off' }}"
- condition: template
value_template: >
{% set brightness = 'input_number.brightness_' ~ trigger.event.data.light_source %}
{{ (state_attr(trigger.event.data.entity_id, 'brightness') | float / 255 * 100) | round(0) != states(brightness) | float }}
sequence:
- service: light.turn_on
data_template:
entity_id: "{{ trigger.event.data.entity_id }}"
brightness_pct: >
{% set brightness = 'input_number.brightness_' ~ trigger.event.data.light_source %}
{{ states(brightness) | float }}
- id: lights_off
alias: Lights Off
mode: parallel
trigger:
- platform: event
event_type: lights_off
condition:
condition: template
value_template: "{{ states(trigger.event.data.entity_id) == 'on' }}"
action:
- service: homeassistant.turn_off
data_template:
entity_id: "{{ trigger.event.data.entity_id }}"

I only validate that the light is off (or on with a different brightness than the one I want) before turning it on, and that is it on before I turn it off. The conditions are not strictly neccesary, they just keep the logs a bit tidier, and you can add e.g. a “notify” or “system_log” as an action to do some debugging etc. without getting flooded with notifications each time the motion sensor is triggered if the lights are already on. The brightness for each light is read from the “input_number” described earlier.

So far, so good. For now this only seems like a few extra steps to get to the action, going through all that “event” sending instead of simply adding all the triggers to the “on” and “off” automations themselves. Someone once said “when in doubt, you can always add another layer of abstraction”, and this looks a bit like that…

But what I realized, was that I sometimes wanted other stuff to be triggered by the same motion sensors. And sometimes I wanted to turn the lights on and off from other automations, but I wanted consistency and easy logging, so my goal was really to have one and only one automation being triggered by any sensor. And have one and only one place to type in the entity_ids for the lights. And have one and only one automation actually turning lights on or off.

Now, that we are about to move to the new OpenZwave-stack I am very happy about that, as there is just very few places I would have to do changes to make all the automations work again, even with new entity_ids all over the place.

Conditions

So how do I add conditions int this mix? I simply have some “meta” automations, that modifies the input_number and input_datetime, and also some that will enable and disable the “lights_on” and “lights_off” automations.

Pause and unpause automations

- id: pause_auto_off
alias: Pause Auto Off
mode: queued
trigger:
- platform: event
event_type: pause_auto_off
condition:
- condition: template
value_template: '{{ states("automation.lights_off_" ~ trigger.event.data.light_source) == "on" }}'
action:
- service: homeassistant.turn_off
data_template:
entity_id: "automation.lights_off_{{ trigger.event.data.light_source }}"
- id: pause_auto_on
alias: Pause Auto On
mode: queued
trigger:
- platform: event
event_type: pause_auto_on
condition:
- condition: template
value_template: '{{ states("automation.lights_on_" ~ trigger.event.data.light_source) == "on" }}'
action:
- service: homeassistant.turn_off
data_template:
entity_id: "automation.lights_on_{{ trigger.event.data.light_source }}"
- id: unpause_auto_on
alias: UnPause Auto On
mode: queued
trigger:
- platform: event
event_type: unpause_auto_on
condition:
- condition: template
value_template: '{{ states("automation.lights_on_" ~ trigger.event.data.light_source) == "off" }}'
action:
- service: homeassistant.turn_on
data_template:
entity_id: "automation.lights_on_{{ trigger.event.data.light_source }}"
- id: unpause_auto_off
alias: UnPause Auto Off
mode: queued
trigger:
- platform: event
event_type: unpause_auto_off
condition:
- condition: template
value_template: '{{ states("automation.lights_off_" ~ trigger.event.data.light_source) == "off" }}'
action:
- service: homeassistant.turn_on
data_template:
entity_id: "automation.lights_off_{{ trigger.event.data.light_source }}"

Again, these could easily be merged into one automation, but why make it complex by adding a lot of conditions and “choose”, when it is just as easy to have a few very simple automations instead.

Set timer and dimmer level

- id: set_lights_off_timer
alias: Set lights off timer
mode: queued
trigger:
- platform: event
event_type: set_lights_off_timer
condition:
- condition: template
value_template: >
{% set timer = 'input_datetime.lights_off_timer_' ~ trigger.event.data.light_source %}
{{ trigger.event.data.off_timer != states(timer) }}
action:
- service: input_datetime.set_datetime
data_template:
entity_id: input_datetime.lights_off_timer_{{ trigger.event.data.light_source }}
time: "{{ trigger.event.data.off_timer }}"
- id: set_dimmer_level
alias: Set dimmer level
mode: queued
trigger:
- platform: event
event_type: set_dimmer_level
condition:
- condition: template
value_template: >
{% set brightness = 'input_number.brightness_' ~ trigger.event.data.light_source %}
{{ trigger.event.data.dimmer_level | int != states(brightness) | int }}
action:
- service: input_number.set_value
data_template:
entity_id: input_number.brightness_{{ trigger.event.data.light_source }}
value: "{{ trigger.event.data.dimmer_level | float }}"

One thing you will notice is that I use a variable called “light_source” quite extensively. Because I have been very consistent in the naming of inputs, entities, automations etc. it means I only need to specify this one name, and can deduce the names of the sensors, input and lights from that.

So now, any other automation can do a simple

- event: pause_auto_on
event_data_template:
light_source: bathroom

or

- event: unpause_auto_on
event_data_template:
light_source: kitchen_table

Or if I want to adjust the dimmer level to 30% and set a timeout of 2 minutes for the toilet lights:

- event: set_dimmer_level
event_data_template:
light_source: toilet
dimmer_level: 30
- event: set_lights_off_timer
event_data_template:
light_source: toilet
off_timer: "00:02:00"

And then I have a lot of simple, easy to understand automations that will do stuff like “pause_auto_on” for the bedroom lights at night and “upause” it at a reasonabe time in the morning. I “pause_auto_off” the bathroom lights when we are showering (high humidity detected), and unpause it again when the humidity is going below a certain threshold. One automation lowers the dimmer_level in the toilet in the evening, and another increases it again in the morning. The off-timer in the kitchen is increased and decreased based on whether it is dinner time or not beacuse we usually don’t trigger the motion sensors while we are sitting down eating…

Variables

I also use a bunch of other inputs as variables in my automations, so I can modify them in one place if I want to adjust some parameters to the scripts.
Here is a snippet of an automation that runs in the morning to adjust some timers and brightness-values:

  action:
# Toilet
- event: set_dimmer_level
event_data_template:
light_source: toilet
dimmer_level: "{{ states('input_number.brightness_medium') }}"
- event: set_lights_off_timer
event_data_template:
light_source: toilet
off_timer: "{{ states('input_datetime.delay_short') }}"
# Bathroom
- event: set_dimmer_level
event_data_template:
light_source: bathroom
dimmer_level: "{{ states('input_number.brightness_medium') }}"
- event: set_lights_off_timer
event_data_template:
light_source: bathroom
off_timer: "{{ states('input_datetime.delay_medium') }}"
# Hallway
- event: set_dimmer_level
event_data_template:
light_source: hallway
dimmer_level: "{{ states('input_number.brightness_high') }}"
- event: set_lights_off_timer
event_data_template:
light_source: hallway
off_timer: "{{ states('input_datetime.delay_long') }}"

If I realize that the “delay_medium” is a bit short, I can change the variable once, and all automations that turns the lights off after that amount of time will be adjusted to the new timeout.

Administration

And at any time, I can very easily see the current status in Lovelace:

Lovelace view of the office lights
  • The lighs are turned on (and has been so for 4 hours)
  • The input_number.brightness is set to 75% (and it was set to this value a day ago)
  • There is a 30 minute timeout after motion is detected before the lights are turned off (and this timeout was changed 2 days ago)
  • The “Lights on”-automation was turned on two hours ago
  • The “Lights off”-automation was turned off just now

So this means that even after 30 minutes of “no motion” in the office, the lights will not be turned off (the automation for that is disabled). But if they are turned off (maybe with the physical switch), they will turn back on (with 75% brightness level) when motion is detected.

I can very quickly find out from the logs what enabled the “lights on” automation two hours ago and what disabled the “lights off” automation 52 seconds ago.

And I never have to go back and try to figure out what the value and state of a bunch of different sensors that together form a long an complex nested list of conditions when I want to find out why the light was turned on (or not) at any time.

If the “turn on”-automation is enabled, the lights will turn on when motion is detected. If I don’t want the lights to be turned on for some reason, something must disable the automation.

And the value of the variables are asy to adjust

Varibale for brightness and timeouts used throughout the automations

To make life even easier, I have a set of other “meta”-automations, so I can do stuff like

- event: set_reenable_time
event_data_template:
automation: lights_on_bathroom
in: 2h
- event: set_reenable_time
event_data_template:
automation: lights_off_bathroom
reenable_time: "2020-08-14 13:19:00"

Which will set another “input_datetime” to the specified time, and when we reach that time, the automation is enabled again.

Reenable time

So the auto_off on the bathroom is currently disabled, but will be reenabled automatically later.

All in all, this makes all my automations pretty short and easy to follow, while I still have all the flexibility I need ensure that lights turn on and off when I want and as I want.

And it can of course easily be extended to also set light colors etc. and to create complete scenes by adding similar automatons for covers and blinds and anything else.

--

--

Ola Thoresen

Noe over gjennomsnittlig interessert. Kjentmann i IP- og nettverksjungelen, og jobber i nLogic AS.