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

I somehow missed the fact of the parallel mode being available to both automations and scripts. I now agree that the same effect could be achieved with one script, and that the syntax is more concise, and that a script seems to be a more logical fit. I appreciate the insights and well thought out feedback. Most appreciated. Thank you.

And scripts? I guess that’s a matter of perspective.

The mode feature is new to scripts (as of 0.113) and is described in the Script integration under the heading Script Modes. The action section of an automation is effectively a script so the mode feature is available to scripts and automations. :wink:

Again, the only reason for my question was to learn if there was some special need to construct this as an automation. It’s unusual to see an automation that triggers itself. It’s clear to me now that this application doesn’t require a trigger and can exist exclusively as a single script.

Would it be much work to convert this to a script?

If you’re interested, I have converted it to a script.

That would be great!

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

hey thanks for creating the script! I wanted to integrate it into my alarm clock, but the brightness doesn’t seem to move above 5 % any ideas what the issue could be? Max is set to 60%

No idea; the script version worked perfectly in all the tests I performed a week ago. Tests included turning on a light and increasing to 100%, increasing from one brightness level to another (by anywhere from 10 to 50%), decreasing by similar amounts, and decreasing to zero (off).

Grasping at straws here but what version of Home Assistant are you using? You need a minimum of version 0.113 for this to work correctly.

I’m using 0.114.4… I’ll try with another light, i’ll report back

i tried with a few other and it doesn’t seem to go over 5%

Edit: it works with hue lights, but not with my lutron lights those don’t go over 5% they have supported features set to 1

lutron_integration_id: 29
friendly_name: 01 Foyer Gruppe 1
supported_features: 1

Sorry, I can’t replicate the problem you are experiencing and that makes it difficult for me to debug it.

I suggest you use mrsnyds original version that is implemented as an automation. When I have more time I will revisit this script and try to think of a scenario that makes it behave like you described.

Just read your edited comment. If it works with Hue lights but not Lutron then it’s a integration-specific issue. I can’t help you with that.

no worries man, thanks anyway!

Trying to get this to work but it fails on the 2nd iteration. Any ideas?

2020-09-04 23:00:16 INFO (MainThread) [homeassistant.components.automation.light_fader_automation] Executing Light Fader Automation
2020-09-04 23:00:16 INFO (MainThread) [homeassistant.components.automation.light_fader_automation] Light Fader Automation: Running script
2020-09-04 23:00:16 INFO (MainThread) [homeassistant.components.automation.light_fader_automation] Light Fader Automation: Choose at step 1: choice 1: Running script
2020-09-04 23:00:16 INFO (MainThread) [homeassistant.components.automation.light_fader_automation] Light Fader Automation: Choose at step 1: choice 1: Executing step light_fader_parameters
2020-09-04 23:00:16 INFO (MainThread) [homeassistant.components.automation.light_fader_automation] Light Fader Automation: Choose at step 1: choice 2: Running script
2020-09-04 23:00:16 INFO (MainThread) [homeassistant.components.automation.light_fader_automation] Light Fader Automation: Choose at step 1: choice 2: Repeating sequence: Iteration 1
2020-09-04 23:00:16 INFO (MainThread) [homeassistant.components.automation.light_fader_automation] Light Fader Automation: Choose at step 1: choice 2: Repeat at step 1: Running script
2020-09-04 23:00:16 INFO (MainThread) [homeassistant.components.automation.light_fader_automation] Light Fader Automation: Choose at step 1: choice 2: Repeat at step 1: Executing step call service
2020-09-04 23:00:16 INFO (MainThread) [homeassistant.components.automation.light_fader_automation] Light Fader Automation: Choose at step 1: choice 2: Repeat at step 1: Executing step delay 0:00:24
2020-09-04 23:00:17 INFO (SyncWorker_24) [pynetgear] Get attached devices
2020-09-04 23:00:24 ERROR (MainThread) [homeassistant.components.automation.light_fader_automation] Light Fader Automation: Choose at step 1: choice 2: Error executing script. Unexpected error for repeat at pos 1: unsupported operand type(s) for /: 'NoneType' and 'int'
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 191, in _async_step
    await getattr(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 488, in _async_repeat_step
    if self._stop.is_set() or not all(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 489, in 
    cond(self._hass, self._variables) for cond in conditions
  File "/usr/src/homeassistant/homeassistant/helpers/condition.py", line 420, in template_if
    return async_template(hass, value_template, variables)
  File "/usr/src/homeassistant/homeassistant/helpers/condition.py", line 400, in async_template
    value = value_template.async_render(variables)
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 230, in async_render
    return compiled.render(kwargs).strip()
  File "/usr/local/lib/python3.8/site-packages/jinja2/environment.py", line 1090, in render
    self.environment.handle_exception()
  File "/usr/local/lib/python3.8/site-packages/jinja2/environment.py", line 832, in handle_exception
    reraise(*rewrite_traceback_stack(source=source))
  File "/usr/local/lib/python3.8/site-packages/jinja2/_compat.py", line 28, in reraise
    raise value.with_traceback(tb)
  File "
TypeError: unsupported operand type(s) for /: 'NoneType' and 'int'
2020-09-04 23:00:24 ERROR (MainThread) [homeassistant.components.automation.light_fader_automation] Light Fader Automation: Error executing script. Unexpected error for choose at pos 1: unsupported operand type(s) for /: 'NoneType' and 'int'
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 191, in _async_step
    await getattr(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 518, in _async_choose_step
    await self._async_run_script(script)
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 526, in _async_run_script
    await self._async_run_long_action(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 342, in _async_run_long_action
    long_task.result()
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 802, in async_run
    await asyncio.shield(run.async_run())
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 183, in async_run
    await self._async_step(log_exceptions=False)
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 191, in _async_step
    await getattr(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 488, in _async_repeat_step
    if self._stop.is_set() or not all(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 489, in 
    cond(self._hass, self._variables) for cond in conditions
  File "/usr/src/homeassistant/homeassistant/helpers/condition.py", line 420, in template_if
    return async_template(hass, value_template, variables)
  File "/usr/src/homeassistant/homeassistant/helpers/condition.py", line 400, in async_template
    value = value_template.async_render(variables)
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 230, in async_render
    return compiled.render(kwargs).strip()
  File "/usr/local/lib/python3.8/site-packages/jinja2/environment.py", line 1090, in render
    self.environment.handle_exception()
  File "/usr/local/lib/python3.8/site-packages/jinja2/environment.py", line 832, in handle_exception
    reraise(*rewrite_traceback_stack(source=source))
  File "/usr/local/lib/python3.8/site-packages/jinja2/_compat.py", line 28, in reraise
    raise value.with_traceback(tb)
  File "
TypeError: unsupported operand type(s) for /: 'NoneType' and 'int'
2020-09-04 23:00:24 ERROR (MainThread) [homeassistant.components.automation.light_fader_automation] While executing automation automation.light_fader_automation
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/automation/__init__.py", line 430, in async_trigger
    await self.action_script.async_run(variables, trigger_context)
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 802, in async_run
    await asyncio.shield(run.async_run())
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 183, in async_run
    await self._async_step(log_exceptions=False)
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 191, in _async_step
    await getattr(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 518, in _async_choose_step
    await self._async_run_script(script)
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 526, in _async_run_script
    await self._async_run_long_action(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 342, in _async_run_long_action
    long_task.result()
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 802, in async_run
    await asyncio.shield(run.async_run())
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 183, in async_run
    await self._async_step(log_exceptions=False)
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 191, in _async_step
    await getattr(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 488, in _async_repeat_step
    if self._stop.is_set() or not all(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 489, in 
    cond(self._hass, self._variables) for cond in conditions
  File "/usr/src/homeassistant/homeassistant/helpers/condition.py", line 420, in template_if
    return async_template(hass, value_template, variables)
  File "/usr/src/homeassistant/homeassistant/helpers/condition.py", line 400, in async_template
    value = value_template.async_render(variables)
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 230, in async_render
    return compiled.render(kwargs).strip()
  File "/usr/local/lib/python3.8/site-packages/jinja2/environment.py", line 1090, in render
    self.environment.handle_exception()
  File "/usr/local/lib/python3.8/site-packages/jinja2/environment.py", line 832, in handle_exception
    reraise(*rewrite_traceback_stack(source=source))
  File "/usr/local/lib/python3.8/site-packages/jinja2/_compat.py", line 28, in reraise
    raise value.with_traceback(tb)
  File "
TypeError: unsupported operand type(s) for /: 'NoneType' and 'int'

Hi all,

First of all, thanks all for creating these. I’ve been looking for something like this for ages and had set up something very crude before but it was nowhere near as good as this.

I really struggled to get it working initially, and I believe it’s to do with the fact that might lights (LightwaveRF) show their brightness as float with up to 2 decimal places.

I ended up changing the “check to see if current brightness matches expected brightness” condition to being a little more flexible like so:

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

It’s not ideal, but it will catch 99% of cases where someone has interrupted the script, and either way it’s much better than i had before (no handling of interuptions at all!).

Just thought I would post here in case anyone else was having similar issues.

Thanks. And if you are looking for an alternative approach, Home Assistant core 0.115 now supports actual variables in your YAML. Blog post here. Updated script documentation here.

Thanks @mrsnyds, I did read that and have started to make use of it in other places, but unfortunately I’m just not quite confident enough with YAML yet to actually apply this new syntax. I’m just happy for now that I’ve got this script going, but will probably tinker with it more going forward.

I tested the script with another light and it works fine. It just does not work with my Yeelight. Not sure why yet.

Thank you for posting your solution. I was trying to make my lights fade and your script made my life much easier. Thank you for that. I made some changes to incorporate variables feature. Instead of branches first script calculates the delay and brightness step and the minion does the actual looping. I’ll try to DRY it a bit.

  # 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
light_fader:
  alias: Light Fader Script
  fields:
    entity_id: 
      description: 'The light to be faded'
      example: 'light.bed_msl100r_main_channel'
    end_level_pct: 
      description: 'Desired ending light level percentage'
      example: 60
    transition:
      description: 'Desired transition time in hh:mm:ss'
      example: 00:15:00
  variables:
    entity_id: 'light.mirror_msl100r_main_channel'
    end_level_pct: 0
    transition: 00:01:00
  
  mode: restart
  
  sequence:
    - service: "logbook.log"
      data:
        name: "light_fader"
        entity_id: "script.light_fader"
        domain: "script"
        message: "entity_id {{entity_id}} end_level_pct {{end_level_pct}} transition {{transition}}"
    - service: script.light_fader_minyon
      data:
        entity_id: "{{ entity_id }}"
        start_level_pct: >-
          {% if state_attr(entity_id, 'brightness') == none -%}
            0
          {%- else -%}
           {{((state_attr(entity_id, 'brightness')/255)*100)|round }}
          {% endif %}
        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" #}
          {# ============================================================  #}
          {##}
          {% if state_attr(entity_id, 'brightness') == none -%}
            {% set start_level_pct = 0 %}
          {%- else -%}
            {% set start_level_pct = 
             ((state_attr(entity_id, 'brightness')/255)*100)|round %}                  
          {%- endif %}      
          {# ============================================================ #}
          {# END_level_pct IS PROVIDED BY EVENT DATA  #}
          {# ============================================================ #}
          {##}
          {% set end_level_pct = end_level_pct|int %} 
          {##}
          {# ============================================================ #}
          {# DERIVE transition_secs FROM EVENT DATA STRING hh:mm:ss #}
          {# ============================================================ #}  
          {##}
          {% set transition_secs = (transition[:2]|int * 3600) + 
            (transition[3:5]|int * 60) + 
            (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(entity_id, 'brightness') == none -%}
            {% set start_level_pct = 0 %}
          {%- else -%}
            {% set start_level_pct = 
             ((state_attr(entity_id, 'brightness')/255)*100)|round %}                  
          {%- endif %}
          
          {% if start_level_pct < (end_level_pct|int) -%}
            1
          {%- else -%}
            -1                 
          {%- endif %}  
                   

light_fader_minyon:
  variables:
    entity_id: 'light.mirror_msl100r_main_channel'
    start_level_pct: 30
    end_level_pct: 0
    delay_milli_sec: 100
    step_pct: 1
  mode: restart
  sequence:
    - service: "logbook.log"
      data:
        name: "light_fader_minyon"
        entity_id: "script.light_fader_minyon"
        domain: "script"
        message: "entity_id {{entity_id}}  start_level_pct {{start_level_pct}} end_level_pct {{end_level_pct}} delay_milli_sec {{delay_milli_sec}} step_pct {{step_pct}}"      
    - 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(entity_id, 'brightness') == none) and
                   (step_pct|int == -1) -%}
                False
              {% elif (state_attr(entity_id, 'brightness') == none) and
                   (step_pct|int == 1) -%}   
                True                                             
              {%- elif (step_pct|int == -1) and
                 ((((state_attr(entity_id, 'brightness')/255)*100)|round)
                 > ((end_level_pct)|int)) -%} 
                True                        
              {%- elif (((state_attr(entity_id, 'brightness')/255)*100)|round)
                 < ((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(entity_id, 'brightness')/255)*100)|round)
                  == (start_level_pct|int + ((repeat.index - 1)*(step_pct|int))))  -%} 
                True                        
              {%- else -%}
                False
              {%- endif %}  
        
        sequence:              
          - service: light.turn_on
            data:
              entity_id: "{{ entity_id }} "
              # brightness_step_pct: "{{step_pct}}"
              brightness_pct: >-
                {% if  ((start_level_pct|int) + 
                  ((repeat.index|int)*(step_pct|int))) <= 0 %}
                   0
                {% elif  ((start_level_pct|int) + 
                  ((repeat.index|int)*(step_pct|int))) >= 100 %}
                   100
                {% else %}
                   {{(start_level_pct|int) + ((repeat.index|int)*(step_pct|int))}}
                {% endif %}
          - delay: 
              milliseconds: "{{ delay_milli_sec|int }}"

Look at the script version I posted above. It contains streamlined templates that eliminate several lines of code. Using that version as a basis, I examined the potential to apply variables to reduce the code even further. The result was a minimal savings and I didn’t bother to post the revised version.

Nice!

Your script helped me to wrap my head around templates!

I still prefer an implementation by a few scripts, an outer script is a declaration and nested scripts hide the implementation. Then an implementation might be changed while the earlier usages of the script are not affected. I have some buggy lights, this way I could implement for them a bit different behavior.

Also, it allows using mode according to the nature of a script.