Scripts, repeat, substrings using repeat.index - only first works?

I’m getting lost somewhere. I have a script that looks like the below. Notice the nested repeats, the inner one is attempting to loop through the lights (this works) but through the individual characters in the “pattern” string. This latter does not.

When looking at the trace, the first iteration of the inner repeat gives color==“R”, but the second thru eighth give a null string. They do not give an error.

Am I wrong that the

color: '{{ pattern[(repeat.index - 1):1] }} '

should pick up 1 character in increasing positions? It does not seem to work. (My intent eventually is, when done, to shift that string by 1 character before the outer loop runs, but just trying to get iteration #1 working now). I’ve tried with and without the parentheses inside the brackets.

landscape_rotate: 
  alias: Landscape Rotate
  sequence: 
    choose: 
      conditions: 
        - condition: state
          entity_id: input_boolean.landscape_christmas_on
          state: "off" 
      sequence: 
          # Turn off so we get a clean start
          - service: script.landscape_off_internal

          - service: input_boolean.turn_on
            entity_id: 
              - input_boolean.landscape_christmas_on

          - variables: 
              floods: 
                - light.flood1
                - light.flood2
                - light.flood3
                - light.flood4
                - light.flood5
                - light.flood6
                - light.flood7
                - light.flood8
              pattern: "RRRRGGGG"
          - repeat: 
              sequence: 
                - repeat: 
                    count: '{{ floods | count }}'
                    sequence: 

                    - variables:
                        flood: '{{ floods[(repeat.index - 1)] }}'
                        color: '{{ pattern[(repeat.index - 1):1] }} '
                    # Red lights 
                    - if: 
                        - condition: template
                          value_template: "{{ color == 'R' }} "
                      then: 
                        - sequence: 
                            - service: light.turn_on
                              target: 
                                entity_id: 
                                  - "{{ flood }}"
                              data:
                                effect: none
                                brightness: 180
                                color_name: Red
                                transition: 3
                      else: 
                        - sequence: 
                            - service: light.turn_on
                              target: 
                                entity_id: 
                                  - "{{ flood }}"
                              data:
                                effect: none
                                brightness: 255
                                color_name: Green
                                transition: 3
                      
                - delay: 
                    seconds: 15

              until:
                - condition: state 
                  entity_id: input_boolean.landscape_christmas_on
                  state: "off"

    default:
      - service: script.landscape_off

I would use a Repeat for Each.

Also, for the definition of color remove the :1 so that you are producing an index instead of an invalid index sequence. Only the first is working because [0:1] is the only valid index sequence that is produced.

- repeat:
    for_each: '{{ floods }}'
    sequence:
      - variables:
          flood: '{{ repeat.item }}'
          color: '{{ pattern[repeat.index - 1] }}'
        # Red lights
      - if: ..... 

Are you not going to end up with minus 1 instead of zero on the first iteration?

No, Repeat index starts at 1

1 Like

Good lord why? (rhetorical)

Every other counter starts at 0.

There are 2 hard problems in computer science: cache invalidation, naming things, and off-by-1 errors.

Leon Bambrick

1 Like

:man_shrugging:

Jinja for’s loop.index also starts at 1… but don’t worry, you can use loop.index0 if you prefer… and it has both options in reverse too. I’m just waiting for an LLM-produced template monstrosity that has all 4 in the same template.

1 Like

First, this seems to work, but could you help me understand why?

isn’t “pattern” a string?

Isn’t a substring shown as X[start:length]?

Is it acting as an array?

This is going to confuse me as I was hoping to do a rotate on that string by something like setting pattern = pattern[1:7] + pattern[0:1] (please ignore my made up syntax, I haven’t gotten that far, but conceptually).

Also apologies – it’s late on the east coast, will research more carefully tomorrow, but any insights into substring vs array notation much appreciated.

Yes, but slicing strings can be done using either the string method slice() or the array slicing method which we’ve shown here.

x[stop]
x[start:stop]
x[start:stop:step]
x[::-1]  #reverse the string

Sigh… thank you. That explains my issue.

I think the thing that I spend the most time for with HA is finding definitive answers for scripting tools. I was excited to find an actual “script syntax” document, but it is about 40% of the syntax, doesn’t include this for example. And there seem to many flavors of templating, with differing features and syntax for different places (and even worse if you add in node-red and esphome).

So I appreciate the quick and correct answers. Thank you.

For future reference:

  • Jinja2 is templating language with python syntax.

  • You can slice strings in python.

  • Jinja2 and python are open-source projects thoroughly documented by their respective maintainers.

  • It’s impractical to duplicate their documentation within Home Assistant’s documentation; only the most relevant functions are included and the balance is up to the user to learn from the source (Jinja2/python) documentation.


FWIW, I learned about string slicing by stumbling across it in examples posted on the forum and then found a good tutorial (see link posted above) that explained its operation in greater detail.

1 Like

That helps. There’s no mention of that on the script syntax, but if I search for templating I do find that mentioned, which gives me a place to start reading. What’s a bit less obvious is the shared variable context – I guess a variable in script becomes a variable in the template language (but not the reverse)?

Could I beg one more pointer though. I thought I had this all set to go but I appear to have a scoping problem.

I quickly found out inside a loop I could not redefine a variable from outside the loop, same with an if statement (and that’s documented well). I want to have the outer loop as it runs continually shift the variable ‘pattern’ which is used in the inner loop.

I first tried just defining it in terms of itself (i.e. below savepat replaced with pattern) then tried saving it in case the variable definition is wiped out before being redefined, neither one works.

Is there a scope change per loop?

Is there some simple way to do the shift as below each time the loop iterates?

I guess I could do some math and mod the repeat.index to produce a shifted value based on total iterations, but now I’m stubborn and want to understand how I could do this?

          - repeat: 
              sequence: 
                - variables: 
                    pattern: >-
                          {% if repeat.index == 1  %} RRRRGGGG
                          {% else %} {{ savepat[7] + savepat[0:6] }}
                          {% endif %}
                - variables: 
                    savepat: '{{ pattern }}'
                - repeat: 
                    count: '{{ floods | count }}'
                    sequence: 
                    - variables:
                        flood: '{{ floods[(repeat.index - 1)] }}'
                        color: '{{ pattern[(repeat.index - 1)] }} '

In your mind, keep “script variable” (what you define in the script’s variables option) separate from “Jinja2 variable” (what you define within a Jinja2 template).

Scoping rules apply to both script and Jinja2 variables.

  • A Jinja variable’s scope is exclusively within the template where it’s defined.

  • A script variable’s scope depends where it’s defined within the script.

    • If you define it at the “top level”, it can be referenced anywhere else in the script.

    • If you define it elsewhere like within a repeat or choose or if-then or anywhere that’s not “top level”, it’s scope is limited to that “block”.


As for your repeat within a repeat, as you have discovered, you cannot redefine a script variable’s value that way (i.e. the inner loop cannot change the value of a variable defined in the outer loop).

The example you posted doesn’t call any actions so it doesn’t benefit from employing repeat. It can be redesigned to be a Jinja2 template. A Jinja2 variable’s value can be redefined by an inner loop provided you define the variable using namespace.

Here’s a contrived example:

{% set ns = namespace(x = 0, y = 0) %}
{% for i in range(0,5) %}
  {% set ns.x = ns.x + i %}
  {% for j in range(0,5) %}
    {% set ns.y = ns.y + j %}
  {% endfor %}
{% endfor %}
{{ ns.x }}
{{ ns.y }}

image

Thank you. I didn’t include the whole script, it does call actions in the inner loop, the other loop just runs forever (with a delay as the last step).

So while I can refer to a variable defined in an outer loop within the inner loop, I cannot change its value in the inner loop.

And I think you are saying that for a script (not template) variable i cannot change its value inside the same loop. And I can’t use a template variable (which I can change) because the template gets broken when I do service calls.

I guess I’m back to doing math based on repeat.index and forming a new value each time. I can do that.

Thank you.

In case it helps anyone else (or if anyone wants to suggest corrections), I think this script works (there are some fairly obvious dependencies but they are not relevant to the looping and pattern shift):

landscape_christmas: 
  alias: Landscape Christmas
  sequence: 
    choose: 
      conditions: 
        - condition: state
          entity_id: input_boolean.landscape_christmas_on
          state: "off" 
      sequence: 
          # Turn off so we get a clean start
          - service: script.landscape_off_internal

          # Turn on indicator we are on/running (this is the only one that will be running)
          - service: input_boolean.turn_on
            entity_id: 
              - input_boolean.landscape_christmas_on

          # Any number of lights can be added here, but then fill in the pattern also with that many 
          - variables: 
              floods: 
                - light.flood1
                - light.flood2
                - light.flood3
                - light.flood4
                - light.flood5
                - light.flood6
                - light.flood7
                - light.flood8
              orig_pattern: "RRRRGGGG"
              numlights: '{{ floods | count }}'
          - repeat: 
              sequence: 
                - variables: 
                    pattern: >-   # this will calculate the rotated location based on repeat.index 
                          {% set modpat = ( (repeat.index - 1) % numlights) %} 
                          {% set pat = orig_pattern[modpat:numlights] + orig_pattern[0:modpat] %}
                          {{ pat }}
                - repeat: 
                    count: '{{ numlights }}' 
                    sequence: 
                    - variables:
                        flood: '{{ floods[(repeat.index - 1)] }}'
                        color: '{{ pattern[(repeat.index - 1):(repeat.index)] }} '     # pull from same spot, but the string rotates each outer loop
                    # Red lights 
                    - if: 
                        - condition: template
                          value_template: "{{ color == 'R' }} "
                      then: 
                        - sequence: 
                            - service: light.turn_on
                              target: 
                                entity_id: 
                                  - "{{ flood }}"
                              data:
                                effect: none
                                brightness: 180
                                color_name: Red
                                transition: 3
                      else: 
                        - sequence: 
                            - service: light.turn_on
                              target: 
                                entity_id: 
                                  - "{{ flood }}"
                              data:
                                effect: none
                                brightness: 255
                                color_name: Green
                                transition: 3
                - delay: 
                    seconds: 6
              until:
                - condition: state 
                  entity_id: input_boolean.landscape_christmas_on
                  state: "off"

    default:
      - service: script.landscape_off