YAML Light Fader for light entities that don't support "transition"

Here is a script version of mrsnyds original work. Be advised this script is not identical to the original automation version. It uses the same algorithm but with several differences:

  • Uses light not entity_id
    The option to specify the light(s) to be dimmed is named light instead of entity_id. The reason is because when you call a script using the script.turn_on service, you specify which script to turn on by setting the service’s entity_id option. This is the same name, so the script’s variable name was changed to light.

  • No need to specify branch
    When calling the script, you do not need to include branch: 'calculate'. The script defaults to ‘calculate’ mode when the branch option is not included.

  • Streamlined templates
    The templates are different. Wherever possible, I reduced code-size. Based on the tests I’ve done, everything continues to work like the original, automation version (but YMMV).

  • Includes fields option
    Each variable is documented so that it appears in Developer Tools > Services.

Here is the script version with all of mrsnyds comments.

Click to reveal commented version
  light_fader:
    description: 'Fades lights to a desired level over a specified transition period.'
    fields:
      light:
        description: Entity_id of light(s) as a list or comma-separated.
        example: light.kitchen, light.bathroom, light.garage     
      end_level_pct:
        description: Integer value from 0 to 100 representing the desired final brightness level.
        example: '80'
      transition:
        description: Transition time for fading indicated in HH:MM:SS format (Hours:Minutes:Seconds).
        example: '00:05:30'
      branch:
        description: "OPTIONAL. If unspecified, default is 'calculate'. 'execute_fade' is reserved for use by the script itself."
        example: calculate
    mode: parallel
    sequence:
      # SUMMARY: (1) The first branch is selected when the triggering event data omits the "branch" option or sets it to "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 the initialization phase which supplies all of the parameters needed to execute the 
      #              light transition (fade in or fade out). Some of the 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 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: "{{ branch is not defined or branch == 'calculate' }}"
            sequence:
              - service: script.turn_on
                entity_id: script.light_fader
                data_template:
                  variables:
                    branch: 'execute_fade'
                    light: "{{ light }}"
                    start_level_pct: >-
                      {% set b = state_attr(light, 'brightness') %}
                      {{ 0 if b == none else (b/255*100)|round }}
                    end_level_pct: "{{ 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" #}
                      {# ============================================================  #}
                      {##}
                      {% set b = state_attr(light, 'brightness') %}
                      {% set start_level_pct = 0 if b == none else (b/255*100)|round %}                  
                      {# ============================================================ #}
                      {# End_level_pct IS PROVIDED BY VARIABLE  #}
                      {# ============================================================ #}
                      {##}
                      {% set end_level_pct = end_level_pct|int %} 
                      {##}
                      {# ============================================================ #}
                      {# DERIVE transition_secs FROM VARIABLE hh:mm:ss #}
                      {# ============================================================ #}  
                      {##}
                      {% set t = transition.split(':') | map('int') | list %}
                      {% set transition_secs = t[0]*3600 + t[1]*60 + t[2] %}    
                      {##}
                      {# ============================================================ #}
                      {# 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 %}
                      {{ 100 if delay_milli_sec <= 99 else delay_milli_sec }}

                    # ======= "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: >
                      {% set b = state_attr(light, 'brightness') %}
                      {% set start_level_pct = 0 if b == none else (b/255*100)|round %}                  
                      {{ 1 if start_level_pct < end_level_pct|int else -1 }}

          #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: "{{ branch is defined and 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: >-
                        {% set b = state_attr(light, 'brightness') %}
                        {% set b = (b/255*100)|round if b != none else b %}
                        {% if b == none -%}
                          {{ step_pct|int != -1 }}
                        {%- elif b > end_level_pct|int -%} 
                          {{ step_pct|int == -1 }}                       
                        {%- else -%}
                          {{ b < end_level_pct|int }} 
                        {%- 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: >-
                        {% set b = state_attr(light, 'brightness') %}
                        {% set b = (b/255*100)|round if b != none else 0 %}
                        {{ b == start_level_pct|int + ((repeat.index - 1) * (step_pct|int)) }}  

                  sequence:
                    - service: light.turn_on
                      data_template:
                        entity_id: "{{ light }} "
                        # brightness_step_pct: "{{step_pct}}"
                        brightness_pct: >-
                          {% set x = start_level_pct|int + (repeat.index|int * step_pct|int) %}
                          {{ ([0, x, 100]|sort)[1] }}
                    - delay: 
                        milliseconds: "{{ delay_milli_sec|int }}"

Here is the script version without comments.

Click to reveal comment-free version
  light_fader:
    description: 'Fades lights to a desired level over a specified transition period.'
    fields:
      light:
        description: Entity_id of light(s) as a list or comma-separated.
        example: light.kitchen, light.bathroom, light.garage     
      end_level_pct:
        description: Integer value from 0 to 100 representing the desired final brightness level.
        example: '80'
      transition:
        description: Transition time for fading indicated in HH:MM:SS format (Hours:Minutes:Seconds).
        example: '00:05:30'
      branch:
        description: "OPTIONAL. If unspecified, default is 'calculate'. 'execute_fade' is reserved for use by the script itself."
        example: calculate
    mode: parallel
    sequence:
      - choose:
          - conditions:
              - condition: template
                value_template: "{{ branch is not defined or branch == 'calculate' }}"
            sequence:
              - service: script.turn_on
                entity_id: script.light_fader
                data_template:
                  variables:
                    branch: 'execute_fade'
                    light: "{{ light }}"
                    start_level_pct: >-
                      {% set b = state_attr(light, 'brightness') %}
                      {{ 0 if b == none else (b/255*100)|round }}
                    end_level_pct: "{{ end_level_pct|int }}"
                    delay_milli_sec: >- 
                      {% set b = state_attr(light, 'brightness') %}
                      {% set start_level_pct = 0 if b == none else (b/255*100)|round %}                  
                      {% set end_level_pct = end_level_pct|int %} 
                      {% set t = transition.split(':') | map('int') | list %}
                      {% set transition_secs = t[0]*3600 + t[1]*60 + t[2] %}    
                      {% set delay_milli_sec = (((transition_secs/(end_level_pct - start_level_pct))|abs)|round(precision=3)*1000)|int %}
                      {{ 100 if delay_milli_sec <= 99 else delay_milli_sec }}
                    step_pct: >
                      {% set b = state_attr(light, 'brightness') %}
                      {% set start_level_pct = 0 if b == none else (b/255*100)|round %}                  
                      {{ 1 if start_level_pct < end_level_pct|int else -1 }}

          - conditions:
              - condition: template
                value_template: "{{ branch is defined and branch == 'execute_fade' }}"
            sequence:
              - repeat:
                  while:
                    - condition: template
                      value_template: "{{ repeat.index <= 102 }}" 

                    - condition: template
                      value_template: >-
                        {% set b = state_attr(light, 'brightness') %}
                        {% set b = (b/255*100)|round if b != none else b %}
                        {% if b == none -%}
                          {{ step_pct|int != -1 }}
                        {%- elif b > end_level_pct|int -%} 
                          {{ step_pct|int == -1 }}                       
                        {%- else -%}
                          {{ b < end_level_pct|int }} 
                        {%- endif %}

                    - condition: template
                      value_template: >-
                        {% set b = state_attr(light, 'brightness') %}
                        {% set b = (b/255*100)|round if b != none else 0 %}
                        {{ b == start_level_pct|int + ((repeat.index - 1) * (step_pct|int)) }}  

                  sequence:
                    - service: light.turn_on
                      data_template:
                        entity_id: "{{ light }} "
                        # brightness_step_pct: "{{step_pct}}"
                        brightness_pct: >-
                          {% set x = start_level_pct|int + (repeat.index|int * step_pct|int) %}
                          {{ ([0, x, 100]|sort)[1] }}
                    - delay: 
                        milliseconds: "{{ delay_milli_sec|int }}"
7 Likes