Add easier solution for loops in automations/scripts

Repeated tasks under the action part of an automation seem to be a quite common, although complicated, task to script. An example would be to blink a light on and off or to switch between two colors of a light x times. The most basic way of doing this would be to copy the on/off parts x times in the automation. A more advanced way seem to be to make two scripts, and have them call the other script at the end of each of these. Both of these seem to be quite cumbersome solutions to an often occurring task in HA.

Would it be possible to add ‘loop’ as a method in an automation/script? For example like this to repeat the corresponding tasks five times:

action:
  loop: 5
    - service: light.turn_on
      entity_id: light.livingroom
    - service: light.turn_off
      entity_id: light.livingroom

Perhaps also with the possible addition of a condition to the loop, which will break the loop if no longer met:

action:
  loop: 100
    - condition: numeric_state
      entity_id: sensor.temperature
      below: '4'
    - service: light.turn_on
      entity_id: light.livingroom
    - service: light.turn_off
      entity_id: light.livingroom

My apologies if a request for this feature already exists, but I could not find one.

I like the idea, the only thing that you’d need in addition to the number of loops is the time element.
So probably something like:

action:
  service: loop.loop
  data:
    loops: 5
    delay_before: 5 # 5 sec to wait before running each loop, should be optional
    delay_after: 10 # 10 sec to wait after running each loop, should be optional
    actions:
      - service: light.turn_on
        entity_id: light.livingroom
      - service: light.turn_off
        entity_id: light.livingroom
1 Like

How about a stop_if: that could be used with or instead of a loop count and uses a similar syntax to automation conditions?
eidt: nevermind, I think thats what oyu meant by the condition

The “delay_before” and “delay_after” can easily be handled by putting in the existing “delay:” command in line with the services in the actions part, and is therefore not necessary:

actions:
  - delay: 00:00:05
  - service: light.turn_on
    entity_id: light.livingroom
  - service: light.turn_off
    entity_id: light.livingroom
  - delay: 00:00:10

This is also more flexible, as you can insert it wherever you like, not just at the beginning or end.

That’s not been my experience and I’ve been automating my home since 2006. In fact, if you need to loop something 100 times, you might want to rethink what you’re attempting to achieve.

The following example has potentially negative implications for lighting systems based on wireless mesh technologies. There’s a minimum time required to propagate the command to the device and receive its acknowledgement. The loop is processed as fast as the host system can execute it. For this application, allowing the loop’s timing to be based on the host’s execution speed is a bad design pattern.

action:
  loop: 5
    - service: light.turn_on
      entity_id: light.livingroom
    - service: light.turn_off
      entity_id: light.livingroom

A superior, deterministic solution, is an event-based design pattern. For example, use a timer to control each iteration of the repeated action (timer duration is 1 second). When the timer starts, it toggles the light, when it finishes it increments a counter. The counter restarts the timer but only if the count is less than the maximum number of iterations. Regardless of the host system’s execution speed, this solution will behave the same way (and provides the targeted device a 1-second window to receive and acknowledge its state-change before the next command arrives).

If you think this is more ‘cumbersome’ than a for-loop, shortcuts always seem easier … at first.

PS
Don’t forget that delay is effectively an in-line timer that suspends execution of the action or script. However, that action/script is still in the process of being executed (even if it is currently suspended). Because you can’t call the same script/action while it is being executed, this can have implications on how and when the script/action may be called.

If you loop something many times, you should also add the means to cancel it. Imagine having a loop: 100 that you want to abort. This is easily done with the timer-based example I provided (set the counter to its maximum value or just cancel the timer). With the proposed system, I see the suggestion stop_if which suggests it depends on another entity’s state (which now becomes a multi-part solution, like the timer-based one).

Thank you for your comment.

The «loop: 100» was an example to highlight that a condition to stop the loop based on external input could be handy, just as you point out in your PS. I can not think of a reason to set up a loop with 100 iterations myself, other than perhaps to flash the lights continuously in case of a fire or as an intruder alert.

The action part of the example was just ment to illustrate how such a loop-function could be formatted in a yaml setting, just like a “lorem ipsum” text is used in the design world, and is not intended as an example to follow without adding the appropriate delays to handle the need for “time required to propagate the command to the device and receive its acknowledgement”, as you put it.

I am not saying that a loop is the way to go for every need in automations, but yes I really do think that setting up a system with timers (which after my understanding calls for a restart of HA while a reload of just the automation.yaml is much faster) and counters spread out on several scripts is overly “cumbersome” when all you really want to achieve is flash a lamp five times. Today I do this by copying the action five times in the automation script (which after my opinion still is a lot easier and transparent than handling it with a timer and counter), but it does lead to an unnecessarily long and bloated yaml-file. Hence the need for a loop function.

1 Like

In one breath you explain ‘flashing lights’ was just lorem ipsum … and then you use the ‘flashing lights’ example, yet again, to justify the use of looping:

when all you really want to achieve is flash a lamp five times

Seems like, other than ‘flashing lights’ it’s hard to invent many other useful applications of repeating the same action over and over. :wink:

Well, the example as is, without implementing delays of any kind, was just lorem ipsum. At the same time the flashing of lights was the thing that got me thinking that a loop would be nice. But I am sure there are other applications for this function. Try taking away the for-next loop out of the toolbox of almost any programmer, and they will probably come after you with a vengeance. It is terribly useful in almost any kind of programming. I am quite sure that if we get the possibility to use loops in automations, people will find new use for it.

One of the great things for me with HA is the flexibility it provides. I can take the parts of HA that suit me, and leave the rest to those who have other needs. I don’t see the point of limiting others just because I don’t find all parts of HA equally interesting. I see this function as useful in MY use of HA, and I have come across many topics in here that tells me I am not alone. We’ll see if enough people vote for this function to make it tempting enough for the kind Gods of HA to implement it. And if they do, you don’t have to use it, but you can if you will. :slight_smile:

That goes without saying.

If you are sure, name three (excluding the beloved flashing lights).

That’s a strawman argument; for-loops exist elsewhere in Home Assistant and there’s been no suggestion to remove them.

New uses for a bad design pattern. An uninterruptible loop controlling state-changes of external devices with no inherent consideration of propagation time (just the hope that some buffer somewhere will queue the flood of state-change requests).

You seem to have a bad day today, I wish you a better one tomorrow. :slight_smile:

4 Likes

LOL! Thanks for the laugh! :+1:

I’m just having a ‘bad design pattern’ day. :wink:

… and I’m still waiting for those three examples.

I also think that this feature could be a great addition for out of the ordinary uses.

There’s something I’m trying to do that would make use of a loop.

I’m trying to control my A/C temperature with a broadlink RF transmitter.
Basically I have a script that sends a “temperature up” command and another for the “temperature down”.
Since I don’t know the actual setpoint of the A/C, I used an input number that I synchronize witch each up/down calls. Let’s call it the actual A/C value.

I have another input_number that is the desired set point.

I’m trying to find a way to make a script that would loop the up or down scripts until the A/C is at the desired set point.

Using a While(condition) loop for this would work great.
The For loop with a condition would also work, I could just put a large number for the loop counter and add a condition.

I managed to get this working with calling scripts but it’s not very efficient and creates a lots of useless scripts that should be only one really.
Also, using that method, I need to add delay: instructions before calling the other script otherwise it fails saying that the script is already in use…
Those delays makes the temperature changes kinda very slowly. I think all that could be better with the use of loops.

1 Like

You may want to use a simple python_script to perform the looping. A python_script offers you even more flexibility than a regular script.

There’s an example shown here:

The challenge was to send repeated commands (via a Broadlink device) to a fan in order to change its speed. The fan has 8 speed levels and one command is used to step through all the levels. The python_script calculates how many times the command must be sent to change from one speed to another. Then it proceeds to send the command the number of times it had calculated.

In this example, each iteration does two things:

  • Execute the broadlink.send service call (using the contents of the variable service_data).
  • Pause for a half-second.
    for i in range(loop):
      hass.services.call('broadlink', 'send', service_data, False)
      time.sleep(0.5)

It will repeat those two commands the number of times specified by the variable loop.

Calling a python_script (from an automation) is similar to calling a regular script. Here’s an example of calling a python_script and passing it a parameter called fan_speed:

          - service: python_script.fan_speed_control
            data:
              fan_speed: '7'

Nice !

That looks like a very complete solution very similar to what I’m doing.

I don’t know Python much, maybe it’s a good place to start, because I will mostly copy all this…

Where do I find the documentation on how to use Python with HA objects ?

The Developer Docs explain it in detail. However, I caution you that it is written for software developers so it is daunting for non-developers. My suggestion is to ‘learn by example’. Simply search the community forum for ‘python_script’ and read the examples created by other people. Copy and paste and experiment.

Tip:
There’s no need to reload python_scripts like you do with scripts and automations. After you make a change to your python_script file, save it and it will be immediately available to Home Assistant. This makes it easier and faster to experiment and learn from one’s mistakes (and successes).

If you want a sensible use-case: my hearing is not so good. If the fire alarm goes off, I want to ring my doorbells (I have 5 of them distributed through the house, one next to my bed).

If I do

  action:
  - data:
      payload: '14163857'
      topic: home/OpenMQTTGateway/commands/MQTTtoSRFB
    service: mqtt.publish

I just hear a ‘ring…’, no way I’m going to wakeup if there ever is a fire.

So now I have

  - data:
      payload: '14163857'
      topic: home/OpenMQTTGateway/commands/MQTTtoSRFB
    service: mqtt.publish
  - delay: 00:00:01
  - data:
      payload: '14163857'
      topic: home/OpenMQTTGateway/commands/MQTTtoSRFB
    service: mqtt.publish

repeated a 100 times. There must be a better solution for this.

Here’s a python_script that will publish the payload 14163857 to the topic home/OpenMQTTGateway/commands/MQTTtoSRFB, pause for 1 second, then repeat this action for a total of 5 times.

fire_alarm.py

service_data = {'topic':'home/OpenMQTTGateway/commands/MQTTtoSRFB', 'payload':'14163857'}
for i in range(5):
  hass.services.call('mqtt', 'publish', service_data, False)
  time.sleep(1)

Your automation would call it as a service:

  action:
     service: python_script.fire_alarm
2 Likes

Thanks .
.

I need to press volume down on my IR blaster 42 times. I would love this.

42 times?

Yeah, the python_script I posted above can handle that need.