I happen to use Lutron Caseta switches, which work great, but the Home Assistant native integration does not support the “transition” feature of the light.turn_on service with these switches. I like automations that gradually transition light levels, so I solved this problem with a Python script that I ran as a service. It accepted starting brightness, ending brightness, and transition time as parameters, and then ran a loop to create the gradual transition. With no looping feature in the HASS script syntax there wasn’t a way to do the necessary repeat actions with YAML … at least not smoothly.
Until release 0.113.0 …
With 113.0 we now have a repea
t action, a choose
action, and automations can be triggered multiple times and run in parallel with the mode
option. This release also brought some performance enhancements that allow for sub-second performance on these repeat actions. All the ingredients are now available to do this in YAML.
This script is a single, reusable automation script that is “callable” with a triggering event
that supplies the necessary event data. I made up a piece of event data named “branch”, which the conditions in the choose
action utilize to select the branch that should run. The flow starts with a triggering event that runs the first branch, which then re-triggers the same automation (thanks to “parallel” mode) to run the second branch.
An event action in this format will trigger the automation:
- event: light_fader_parameters
event_data:
# The fader automation will choose the branch based on this value
branch: calculate
# The light to be faded
entity_id: light.your_fading_light
# Desired ending light level percentage
end_level_pct: 60
# Desired transition time in hh:mm:ss
transition: 00:15:00
And below is the automation that will consume this event and do the light transition. It is pretty verbose with comments, so hopefully it is easy enough to follow. Been testing this for a few days and it seems to work pretty well. My primary use case is for slow transitions, so I haven’t optimized it for faster transitions of less than 15 seconds (I’ll leave that to you!)
Questions, comments, feedback always welcomed.
# This automation consumes an event that includes the data needed
# to drive a gradual light transition. This scripting utilizes
# the new "run mode", "looping", & chooser features introduced in HASS 113.0
id: light_fader
alias: Light Fader Automation
initial_state: 'on'
trigger:
- platform: event
event_type: light_fader_parameters
mode: parallel
action:
# SUMMARY: (1) The first branch is seleced when the triggering event data includes "trigger.event.data.branch == 'calculate'.
# The additional data that is included with the triggering event is used to calculate all the parameters needed
# to execute a repeat loop that transitions the light brightness from one level, to another, in a given amount of time.
# (2) The first branch is essentially just one "event" action which supplies all of the parameters needed to execute the
# light transition (fade in or fade out). Some of the event data is a pass through from the original triggering event,
# but three values - "start_level_pct", "delay_milli_sec", and "step_pct" - are calculated with templates.
# By including trigger.event.data.branch == 'execute_fade' in the event data, the second branch is selected in this automation.
# (3) The repeat loop in the second branch executes until the desired "end_level_pct" is reached.
- choose:
#FIRST BRANCH "calculate" ... derive values from the data in original trigger and pass them to second branch, "execute_fade"
- conditions:
- condition: template
value_template: "{{trigger.event.data.branch == 'calculate' }}"
sequence:
- event: light_fader_parameters
event_data_template:
branch: execute_fade
entity_id: "{{ trigger.event.data.entity_id }}"
start_level_pct: >-
{% if state_attr(trigger.event.data.entity_id, 'brightness') == none -%}
0
{%- else -%}
{{((state_attr(trigger.event.data.entity_id, 'brightness')/255)*100)|round }}
{% endif %}
end_level_pct: "{{ trigger.event.data.end_level_pct|int }}"
# ======= "delay_milli_sec" calculation notes ==============
# The basic calculation for delay_milli_sec could result in either a positive
# or negative value, but the delay must always be a positve value - no matter
# which direction the fade is going - so last step uses "abs" for absolute value.
delay_milli_sec: >-
{# ============================================================ #}
{# start_level_pct MUST BE "0" IF BRIGHTNESS ATTRIBUTE IS "none" #}
{# ============================================================ #}
{##}
{% if state_attr(trigger.event.data.entity_id, 'brightness') == none -%}
{% set start_level_pct = 0 %}
{%- else -%}
{% set start_level_pct =
((state_attr(trigger.event.data.entity_id, 'brightness')/255)*100)|round %}
{%- endif %}
{# ============================================================ #}
{# END_level_pct IS PROVIDED BY EVENT DATA #}
{# ============================================================ #}
{##}
{% set end_level_pct = trigger.event.data.end_level_pct|int %}
{##}
{# ============================================================ #}
{# DERIVE transition_secs FROM EVENT DATA STRING hh:mm:ss #}
{# ============================================================ #}
{##}
{% set transition_secs = (trigger.event.data.transition[:2]|int * 3600) +
(trigger.event.data.transition[3:5]|int * 60) +
(trigger.event.data.transition[-2:]|int) %}
{##}
{# ============================================================ #}
{# CALCULATE & RETURN delay_milli_sec ... min set to 100ms #}
{# ============================================================ #}
{% set delay_milli_sec = (((transition_secs/(end_level_pct - start_level_pct))
|abs)|round(precision=3)*1000)|int %}
{% if delay_milli_sec <= 99 -%}
100
{%- else -%}
{{ delay_milli_sec }}
{%- endif %}
# ======= "step_pct" calculation notes ==============
# Compare start level to end level to see if we increasing or decreasing the brightness
# by 1% with each loop. Must repeat the same "if" templating as in delay_milli_sec for start_level_pct
step_pct: >
{% if state_attr(trigger.event.data.entity_id, 'brightness') == none -%}
{% set start_level_pct = 0 %}
{%- else -%}
{% set start_level_pct =
((state_attr(trigger.event.data.entity_id, 'brightness')/255)*100)|round %}
{%- endif %}
{% if start_level_pct < (trigger.event.data.end_level_pct|int) -%}
1
{%- else -%}
-1
{%- endif %}
#SECOND BRANCH ... accepts values derived in the first branch, as well as "pass throughs" from
# the original trigger, and then actually does the light transition.
- conditions:
- condition: template
value_template: "{{trigger.event.data.branch == 'execute_fade' }}"
sequence:
- repeat:
while:
# Terminates at set # of iterations ... just in case
- condition: template
value_template: "{{ repeat.index <= 102 }}"
# Check to confirm that current brightness has not yet reached end_level_pct
- condition: template
value_template: >-
{% if (state_attr(trigger.event.data.entity_id, 'brightness') == none) and
(trigger.event.data.step_pct|int == -1) -%}
False
{% elif (state_attr(trigger.event.data.entity_id, 'brightness') == none) and
(trigger.event.data.step_pct|int == 1) -%}
True
{%- elif (trigger.event.data.step_pct|int == -1) and
((((state_attr(trigger.event.data.entity_id, 'brightness')/255)*100)|round)
> ((trigger.event.data.end_level_pct)|int)) -%}
True
{%- elif (((state_attr(trigger.event.data.entity_id, 'brightness')/255)*100)|round)
< ((trigger.event.data.end_level_pct)|int) -%}
True
{%- else -%}
False
{%- endif %}
# Check to confirm that current brightness is at the expected level for this iteration.
# If during a long fade (e.g. 10 minutes) someone manually adjusted the brightness,
# the current brightness will not match what the loop expected, and it will stop,
# respecting the manual intervention of a person who is setting their preference.
- condition: template
value_template: >-
{% if repeat.first -%}
True
{%- elif ((((state_attr(trigger.event.data.entity_id, 'brightness')/255)*100)|round)
== (trigger.event.data.start_level_pct|int + ((repeat.index - 1)*(trigger.event.data.step_pct|int)))) -%}
True
{%- else -%}
False
{%- endif %}
sequence:
- service: light.turn_on
data_template:
entity_id: "{{ trigger.event.data.entity_id }} "
# brightness_step_pct: "{{trigger.event.data.step_pct}}"
brightness_pct: >-
{% if ((trigger.event.data.start_level_pct|int) +
((repeat.index|int)*(trigger.event.data.step_pct|int))) <= 0 %}
0
{% elif ((trigger.event.data.start_level_pct|int) +
((repeat.index|int)*(trigger.event.data.step_pct|int))) >= 100 %}
100
{% else %}
{{(trigger.event.data.start_level_pct|int) + ((repeat.index|int)*(trigger.event.data.step_pct|int))}}
{% endif %}
- delay:
milliseconds: "{{ trigger.event.data.delay_milli_sec|int }}"