Stop script when it's called again

I’d like a way to break a script out of a Repeat loop if the script is called again.

It’s not Restart

Sure, you can use Restart, but that’s global. I want to use Parallel with Restart based on the passed-in fields. If the fields are the same as before, stop the script. I’d need some way to group the criteria that’d cause a restart.

An Example Script

I have a script that looks like this:

I need it to run in Parallel because there are different sets of lights that might turn on and need to respond to lighting changes. At the same time, I’ll run into memory leaks if I never turn off the lights.

If, for instance, I press a Zigbee button 30 times to “turn on lights”, it will activate this script 30 times, flooding my Parallel queue until I turn off the lights.

Possible Solution

I was thinking of listening to call_service making sure to check for my script name and set of lights, but I’m not sure how to do that.

I can do something like this:

trigger:
  - platform: event
    event_type: call_service
    event_data:
      domain: script
      service: adaptive_lighting

But I’m not even sure if this will work, nor how to narrow it down to only these lights.

In the Wait For, I can wait for this event to trigger regardless of which lights. That’ll trigger the Repeat Until condition, and I can add something there for:

{% set called_lights = trigger.event.data.service_data.lights | sort %}
{% set expected_lights = lights | sort %}
{{ called_lights == expected_lights }}

This template will error unless a trigger occurs, so I might need to say something like if trigger && trigger.event && trigger.event.data etc unless there’s an easier way to do that.

Horrible Solution

Another idea is to enable a toggle before calling a script that, if true, stops other running scripts. Then I call the script.

Since there’s no bulk-adding of Helpers in Home Assistant, and since it’s a maintenance nightmare, I’d like to avoid this method. I have hundreds of lights and 20 lighting rooms.

You can use choose in the script and then hit a stop …

1 Like

Just call the script.turn_off service.


EDIT: that wont work.

What I’d do in your situation is I’d create a trigger based template entity that worked on custom events. Each event would add or remove a string from an attribute in the template entity. I would likely make the attribute a set to easily add/remove/check the value.

In the variable section in your automation, I’d create a hash built from all the entity_id’s this parallel run will act upon.

The condition on the automation would check to make sure the hash does not exist in the set in the template entity.

The first action in the automation would run a custom event that would add the hash to the template entity.

The last action in the automation would run a custom event that would remove the hash from the template entity.

The hardest part will be to figure out what to do during startup and template reloading.

1 Like

This is how I do it - basically if it’s what the previous result was (hold those in a text helper after a successful run) - don’t do it.

1 Like

Great comments! I didn’t know about the Stop action @Neil_Brownlee. Super helpful!

@petro @Neil_Brownlee The idea of using global state with a template and adding strings right? :thinking:.

That might be good. I actually have 2 repeating scripts for light control, and I wanna halt both if a new light action is taken.

As you suggested, instead of halting a running script, I could simply prevent a new one from running, but I’d still have to trigger the current script to repeat the loop when this occurs.

Can I use a better way of storing which scripts are running? Maybe I pass in an ID or something when running the script that also exists in a Multi-Select? I want it tied to the Automation, but it’s flawless if I compare against lights though.

Thinking about this ID aspect more, the lights variable changes. For instance, my theater and kitchen are next to each other. They share lights until something’s playing, then the theater is controlled separately.

In that case, I can’t rely on the lights field because it will only include the kitchen, not the theater.

Because lights may change for a given automation, the proper solution is it pass an ID from the automation. This is literally how I did it in code with RxJS and Redux-Observable 8 years ago. Looks like my professional skills are still required for Home Assistant :joy:.

I tried doing checking if one execution was already running, but if lights is different this time around, then I need to kill off the old one as the lights field is no longer valid.

I’m not quite sure how to fix this, but it goes back to my original question, and I’m gonna need that Stop thing.

Here’s my plan:

  1. Add an id field to each lighting script call.
  2. When starting a new lighting script, dispatch some event with the ID.
  3. Set up a trigger in that Repeat for if that new event is called. If so, and the ID matches, stop execution.

I hope that works. This is gonna be hard to test since events suck to deal with in Automations.

This worked!

But it’s not the end of it.

I found another bug I’m working on as this script is actually called by another long-running script.

Timing issues

I’m running into a really weird timing issue. I can cancel this, but only once the trigger is running. If I start up another while lights are being turned on, it won’t hit my “Wait for”; therefore, it won’t trigger and won’t stop multiple instances from running.

I tried using Parallel sequences to run the loop and kill the script, but the Stop action only stops that sequence. So when running in parallel, the other sequence (the loop) won’t stop.

I thought of creating a local variable, but you can’t listen to changes in local variables, and modifying the local variable only does it in that particular scope.

Best I can see is firing an event when the stop action needs to be called, but I need to pass it a unique ID. That unique ID can be created when the script starts.

Yeah, we really need a native Home Assistant way to have a Restart mode with some identifier.

I tried multiple things, and there’s no action that’s “do this stuff unless something happens during execution”.

One of my scripts is working “correctly”. If I start the script 12 times in a row, I get 12 running instances, but if I start it again, it kills the others.

The other script though, it’s getting stuck. If I start too many, sometimes one stays around. It’s calling my other long-running script, so if that other long-running script stops too early, then it just gets stuck there.

Normal running

The {} icon is the “wait for”:

Stopped by duplicate call

This goes through my condition which checks if it was a duplicate call and logs that. The repeat isn’t triggered a second time:

Weird in-between state

This one never made it to the “wait for” and gets stuck here:

I think it had something to do with the script ending its own duplicate execution.

After much work, I finally got it!

Using a Parallel action with Stop doesn’t work if the Repeat is in a separate sequence, but if the Stop occurs in Repeat even if it’s parallel execution, then it works fine!

Normal

Stopped

Another idea

I had another idea, and that was to use script.turn_on for asynchronous script execution. That would allow me to execute the script and immediately hit the “Wait for”.

I decided against it because the script.turn_on UI is jank, and this parallel method works a lot better!

Weirdness (might want script.turn_on)

One thing that’s weird, my parent script that calls the long-running script, when it executes, it’ll wait for that script to finish execution right?

If that’s the case, it’ll wait forever because that script doesn’t finish. That might be a good case for using script.turn_on, but the good thing is, having this in Parallel mode with “Wait for” actually means a trigger will force it to Repeat. In that case, it’ll cancel out of any running scripts :+1:.

What do you guys think?

1 Like