The quantity of automations (and associated entities) needed to create a solution is entirely at your discretion. Basically, do whatever is easier for you to create and maintain.
For example, I have a single automation that handles the control of two lights (interior/exterior) whenever the garage door is open/closed after dark. It monitors several entities to do its job:
binary_sensor for the garage door (contact sensor)
timer for determining when to turn off interior/exterior lights
binary_sensor for garage occupancy (motion sensor)
Here’s what that single automation does:
If it’s after sunset and the garage door opens:
Turn on the garage light and the exterior light (but only if they are currently off).
Start the timer with 10 minutes.
If it’s after sunset and the garage door closes:
Start the timer with 2 minutes.
When the timer expires:
Turn off the exterior light.
If the garage is unoccupied, turn off the garage light.
If the garage is occupied, wait for it to become unoccupied before turning off the garage light.
Nice. How are you implementing “garage occupancy” using a binary_sensor? I assume there would be some time delay after last motion sensed before declaring “unoccupied”. Same with garage door, although this one is probably easier since either the door is open, or it’s not. Insights appreciated!
I have a switch in the garage, to control the garage light, that also has built-in sensors reporting the following:
temperature
motion
occupancy
“Occupancy” is reported as:
on the moment it detects motion
off after at least 1 continuous minute of no motion detected
FWIW, if you have an existing motion sensor, you can use it in a Template Binary Sensor to report occupancy. The template would report off only if the time difference between now and the last time the motion sensor had changed to off is greater than 1 minute (or more; your choice).
The garage door’s state, open/closed, is detected by a (hard-wired) magnetic contact sensor
This is the exact tradeoff I’m trying to weigh, and I’m going to explore this some more. I was reading a bunch of threads last night and came across this one:
and was interested specifically in this quote:
Maybe I’m missing the context, but it seems like your template sensor would only be able to rely to now() to update (there isn’t anything else changing). Can you post your template sensor for reference so I have a better idea of what exactly is being registered and how it works?
As mentioned, I use the switch’s built-in occupancy sensor. What I described is how to emulate one using a Template Binary Sensor. At the moment, I don’t have the time to create and test the template for you.
Here are my automations for controlling my bathroom vanity light just as an example of what you can do. This particular light is controlled by motion, timers, occupancy mode, illuminance.
Bathroom Light Automation
- id: light_upstairs_bathroom_motion_lights
alias: '[Light] Upstairs Bathroom Motion Lights'
description: Turn on lights when motion detected in bathroom.
initial_state: true
mode: restart
trigger:
- platform: state
entity_id: binary_sensor.upstairs_bathroom_sensor_motion
to: 'on'
condition:
- condition: state
entity_id: input_boolean.light_automation
state: 'on'
- condition: state
entity_id: input_boolean.alarm_triggered
state: 'off'
- condition: or
conditions:
- condition: state
entity_id: binary_sensor.auto_light_on
state: 'on'
- condition: numeric_state
entity_id: sensor.upstairs_bathroom_sensor_illuminance
below: 50
action:
- choose:
- conditions:
- condition: state
entity_id: input_select.occupancy_mode
state: Night
sequence:
- condition: state # in sequence so we don't trigger default if timer on
entity_id: timer.upstairs_bathroom_vanity_light
state: idle
- service: light.turn_on
entity_id: light.upstairs_bathroom_vanity_rgb_light
data:
profile: red_dim
default:
- choose:
- conditions:
- condition: state
entity_id: binary_sensor.quiet_hours
state: 'on'
sequence:
- condition: state # in sequence so we don't trigger default if timer is on
entity_id: timer.upstairs_bathroom_vanity_light
state: idle
- service: light.turn_on
data:
entity_id: light.upstairs_bathroom_vanity_rgb_light
brightness: 40
#OPTION profile: sunrise_low
default:
- service: light.turn_on
data:
entity_id: light.upstairs_bathroom_vanity_rgb_light
brightness: 125
#OPTION profile: warm
- service: light.turn_on
data:
entity_id: light.upstairs_bathroom_shower_light
brightness: 70
#######################################################################################################################
## Light - Upstairs Bathroom Vanity Light Auto Off
#######################################################################################################################
- id: light_upstairs_bathroom_vanity_light_auto_off #OCC
alias: '[Light] Upstairs Bathroom Vanity Light Auto Off'
description: Turn off bathroom vanity light.
initial_state: true
trigger:
- platform: state # specify from state, unknown at startup
entity_id: binary_sensor.upstairs_bathroom_sensor_motion
to: 'off'
from: 'on'
for:
minutes: 5
- platform: state
entity_id: input_select.occupancy_mode
to:
- Away
- Vacation
- Night
for:
minutes: 2 # allow timer to cancel, occupant to leave
- platform: state # req in case motion/timer expire during restart
entity_id: input_boolean.startup_pending
to: 'off'
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.upstairs_bathroom_vanity_light
condition:
- condition: state
entity_id: input_boolean.light_automation
state: 'on'
- condition: template
value_template: "{{ is_state('input_boolean.presence_automation','on') if trigger.entity_id == 'input_select.occupancy_mode' else true }}"
- condition: state
entity_id: input_boolean.alarm_triggered
state: 'off'
- condition: state
entity_id: timer.upstairs_bathroom_vanity_light
state: idle
- condition: state
entity_id: binary_sensor.upstairs_bathroom_sensor_motion
state: 'off'
action:
- choose:
- conditions:
- condition: state
entity_id: input_select.occupancy_mode
state: Night
sequence:
- service: light.turn_on # turn on the light back to night mode setting
data:
entity_id: light.upstairs_bathroom_vanity_rgb_light
profile: red_min
transition: 2
default:
- service: light.turn_off
data:
entity_id: light.upstairs_bathroom_vanity_rgb_light
transition: 1
#######################################################################################################################
## Light - Upstairs Bathroom Vanity Light Timer Finished
#######################################################################################################################
- id: light_upstairs_bathroom_vanity_light_timer_finished
alias: '[Light] Upstairs Bathroom Vanity Light Timer Finished'
description: Turn light flux back on.
initial_state: true
trigger:
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.upstairs_bathroom_vanity_light
condition:
- condition: state
entity_id: input_boolean.light_automation
state: 'on'
- condition: state
entity_id: script.start_shower
state: 'off'
action:
- service: switch.turn_on
entity_id: switch.light_flux_bathroom
Thanks. BTW, I just downloaded Sun2 and I’m already super-impressed. I downloaded it because I want to try writing state-driven lighting rules instead of event-driven, and it’s the easiest way to get today’s sunset. Thanks for making this.
Based on all of the above, as well as what I read in multiple other threads, this is what I came up with in order to:
a) minimize the number of extra entities
b) consolidate rules that affect the same entity
c) avoid duplication of code as much as possible
I debated using a template sensor to calculate the correct state of the driveway light, paired with an automation to turn the light on/off, so that I could rely on the automatic event registration to allow entities only to be referenced once. I elected to go with a single automation instead, just to avoid the extra sensor.
- id: '12345'
alias: Driveway Light Automation
description: ''
trigger:
- platform: time_pattern
minutes: /1
- platform: state
entity_id: binary_sensor.driveway_dusk_dawn
- platform: state
entity_id: input_datetime.driveway_motion_triggered_time
- platform: state
entity_id: cover.garage_door_1
- platform: state
entity_id: cover.garage_door_2
condition: []
action:
- service: light.turn_on
data:
brightness: >-
{% set LIGHT_DELAY_SEC = 120 %}
{% if
as_timestamp(now()) >= as_timestamp(states('sensor.sunset')) or
now() < now().replace(hour=02).replace(minute=00).replace(second=00).replace(microsecond=00) or
( (is_state('binary_sensor.driveway_dusk_dawn', 'on') or
as_timestamp(now()) < as_timestamp(states('sensor.sunrise'))
) and
(as_timestamp(now()) - as_timestamp(states('input_datetime.driveway_motion_triggered_time')) <= LIGHT_DELAY_SEC or
is_state('cover.garage_door_1', 'open') or
as_timestamp(now()) - as_timestamp(states.cover.garage_door_1.last_changed) <= LIGHT_DELAY_SEC or
is_state('cover.garage_door_2', 'open') or
as_timestamp(now()) - as_timestamp(states.cover.garage_door_2.last_changed) <= LIGHT_DELAY_SEC
)
)
%}
255
{% else %}
0
{% endif %}"
entity_id: light.driveway_light
mode: single
Translation:
Re-evaluate the state of the light on:
Every minute
Change of dusk/dawn state
Change of driveway motion trigger time
Change of garage door 1 state
Change of garage door 2 state
In order to consolidate on and off into a single automation, I used the trick to universally call the light.turn_on service and only change the brightness to represent on and off (255 and 0 respectively). The logic represents when the light should be on (otherwise it should it be off)
Set a constant of 120 seconds as delay before turning off the light
The light should be on unconditionally from sunset to 2am
The time is after today’s sunset (and before midnight as implied by how this sunset sensor is updated) OR
Is there a cleaner way to do a time comparison with a fixed time?
Other conditions can turn the light on only if it’s dark outside
The time is before sunset (and after midnight as implied by how the sunrise sensor is updated) OR
The dusk sensor indicates that its dark
AND
The driveway motion sensor has triggered within the last 120 seconds OR
The garage door 1 is currently open OR
The garage door 1 has changed state within the last 120 seconds OR
The garage door 2 is currently open OR
The garage door 2 has changed state within the last 120 seconds
I would really appreciate feedback on this approach and any ideas for optimizations. I tried to make relative_time work but I think I resolved that it only produces a string that is impractical for comparison.
Also, I don’t understand states('cover.garage_door_1') vs. states.cover.garage_door_1 other than that the documentation indicates to avoid the latter whenever possible. However I’m unable to access last_changed any other way, failing to get the states() and state_attr() functions to retrieve it.
There were a lot of DateTime comparisons with now() which I factored out into a macro called delta(). It takes a DateTime and converts it to a timestamp and subtracts now() as a timestamp to produce a delta. A negative result indicates that the given DateTime is in the past and a positive result indicates that it is in the future.
A variable NOW is set to zero and defined somewhat redundantly, but I think it makes the script a bit more readable: NOW >= delta(states('sensor.sunset')) | int is much more understandable as “if it is after sunset” much easier than delta(states('sensor.sunset')) | int <= 0
The same macro can be used for the LIGHT_DELAY_SEC calculations by adding the offset to the delta and comparing to NOW.
I also added the whitespace eliminators to every {% %} pair. Is there a downside to doing this all the time (assuming you don’t actually care about whitespace)?
brightness: >
{%- macro delta(dt) -%}{{ as_timestamp(dt) - as_timestamp(now()) }}{%- endmacro -%}
{%- set NOW = 0 -%}
{%- set LIGHT_DELAY_SEC = 120 -%}
{%- if
(NOW >= delta(states('sensor.sunset')) | int or
NOW < delta(now().replace(hour=02).replace(minute=00).replace(second=00).replace(microsecond=00)) | int
) or
( (is_state('binary_sensor.driveway_dusk_dawn', 'on') or
NOW < delta(states('sensor.sunrise')) | int
) and
(NOW < delta(states('input_datetime.driveway_motion_triggered_time')) | int + LIGHT_DELAY_SEC or
is_state('cover.garage_door_1', 'open') or
NOW < delta(states.cover.garage_door_1.last_changed) | int + LIGHT_DELAY_SEC or
is_state('cover.garage_door_2', 'open') or
NOW < delta(states.cover.garage_door_2.last_changed) | int + LIGHT_DELAY_SEC
)
)
-%}
255
{%- else -%}
0
{%- endif -%}
Also, any thoughts about how I could get inline comments within the if statement? Not sure if Jinja allows this.
Jinja is definitely not my favorite, but not sure how to do this in multiple automations that are knowledgeable of each other such that they don’t stomp over the others’ actions.
Maybe organization is the key. I hate having a million automations, input_datetimes, other entities, etc. that are logically related and can’t be groups/defined together. Any thoughts on this to make the definition of additional automations more palatable?
Well, this a different way to do it, but it seems weird to break up the building of a string literal which references a service call with a very complex, multiline conditional, instead of a simple numeric value. I did debate between the two though.
It’s only weird because you are unfamiliar with it. It’s a long-standing practice. If you search the forum you will find many examples of templated service calls.
FWIW, not too long ago there was a distinction between service_template and service but then templating capability was added to service and the other option name was deprecated.