Thought I’d share my take on this classic automation of turning on the porch lights. This was my first ever automation, and my version of it has evolved over the past four years. Here are my latest set of requirements:
- Be ‘on’ between 30 minutes before sunset and 30 minutes after sunrise
- EXCEPT if it’s after 23:30h or before 07:00h (so ‘off’ in this case)
- EXCEPT if (so, ‘on’ in these cases):
- An exterior door is open
- It is within 30 minutes of an exterior door being closed
- it is within 30 minutes of somebody arriving home
So, first, I have a binary sensor that will be ‘on’ if these conditions are met, and ‘off’ otherwise. I found it frustrating that sun.sun
always shows the next sunrise and not the last sunrise. I approximated by just forcing today’s date onto the setting and rising times; I’m not sure if I’ll run into a “1-hour-off” bug during daylight savings time changes.
template:
- binary_sensor:
- name: outside_lights_on
state: >
{%- set current_time = states('sensor.date_time') | as_timestamp -%}
{%- set today_date = states('sensor.date') -%}
{%- set today_sunset = (today_date + 'T' + state_attr('sun.sun', 'next_setting').split('T')[1]) | as_timestamp - (60 * 30) -%}
{%- set today_sunrise = (today_date + 'T' + state_attr('sun.sun', 'next_rising').split('T')[1]) | as_timestamp + (60 * 30) -%}
{%- set morning_time = (today_date + "T07:00:00") | as_timestamp -%}
{%- set evening_time = (today_date + "T23:30:00") | as_timestamp -%}
{%- set exterior_doors = ['binary_sensor.frontdoor', 'binary_sensor.backdoor'] -%}
{# returns a JSON array of entities that have a state in the *states_to_match* array #}
{%- macro filter_by_state (entities, states_to_match) -%}
{{ "[" }}
{%- for e in entities if (states(e) in states_to_match) -%}
"{{ e }}"{{ "," if (not loop.last) else "" }}
{%- endfor -%}
{{ "]" }}
{% endmacro %}
{# returns a JSON array of entities whose states have changed within *delta* time of *ct* (current_time) #}
{%- macro changed_in_last (entities, ct, delta) -%}
{{ "[" }}
{%- for e in entities if ((ct - (states[e].last_changed | as_timestamp)) < delta.total_seconds()) -%}
"{{ e }}"{{ "," if (not loop.last) else "" }}
{%- endfor -%}
{{ "]" }}
{% endmacro %}
{%- if (current_time > today_sunset) or (current_time < today_sunrise) -%}
{%- if (current_time >= evening_time) or (current_time <= morning_time) -%}
{%- set people_home = states.person | selectattr('state', 'eq', 'home') | map(attribute='entity_id') | list -%}
{%- set people_home_in_range = changed_in_last (people_home, current_time, timedelta(minutes=30)) | from_json -%}
{%- set doors_open = filter_by_state(exterior_doors, ['on']) | from_json -%}
{%- set doors_closed = filter_by_state(exterior_doors, ['off']) | from_json -%}
{%- set doors_closed_in_range = changed_in_last (doors_closed, current_time, timedelta(minutes=30)) | from_json -%}
{%- if (people_home_in_range | count > 0) -%} true
{%- elif (doors_open | count > 0) -%} true
{%- elif (doors_closed_in_range | count > 0) -%} true
{%- else -%} false
{%- endif -%}
{%- else -%}
true
{%- endif -%}
{%- else -%}
false
{%- endif -%}
Next, I have an automation to set the state of the outside lights to whatever the sensor says they should be. Ignore the condition
for now, I’ll get back to that.
automation:
- alias: outside_lights_on_off
mode: queued
max: 10
max_exceeded: silent
initial_state: 'on'
trigger:
- platform: state
entity_id: binary_sensor.outside_lights_on
action:
- condition: template
value_template: "{{ not is_state('timer.porchlights', 'active') }}"
- service: "light.turn_{{ states('binary_sensor.outside_lights_on') }}"
data:
entity_id:
- light.front_porch_lights
- light.garage_outside_light
- light.deck_light
After trying this out for a night, I realized that there was a problem: if somebody manually changed the state of any of those lights, it would never come back in-line with the rest until the binary sensor changed state again. This is where that timer comes in. I introduced it along with two other automations to allow people to manually turn the lights on or off outside of their scheduled time ranges. When the lights are manually switched, start a 20 minute timer, after which another automation will set the lights back to the state the binary sensor says they should be:
timer:
porchlights:
duration: '00:20:00'
automation:
- alias: outside_lights_manually_switched
mode: restart
trigger:
- platform: state
entity_id:
- light.front_porch_lights
- light.garage_outside_light
- light.deck_light
condition:
condition: template
value_template: >
{# Be careful not to trigger this automation when the lights are turned on or off via the actual schedule. #}
{%- set two_seconds_ago = (now() - timedelta(seconds=2)) | as_timestamp -%}
{%- set last_automation_run = state_attr('automation.outside_lights_on_off', 'last_triggered') | as_timestamp -%}
{{ last_automation_run < two_seconds_ago }}
action:
- service: timer.cancel
target:
entity_id: timer.porchlights
- service: timer.start
target:
entity_id: timer.porchlights
data:
duration: "00:20:00"
- alias: outside_lights_timer_finished
trigger:
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.porchlights
action:
- service: automation.trigger
target:
entity_id: automation.outside_lights_on_off
I’m pretty happy with this version; it covers all the cases I could think of.