How to stop a script from within the script itself..?

how can we stop a script, that has several other sub actions and repeats, to finally end itself?

  play_sleep_radio:
    alias: Play sleep radio
    mode: restart
    sequence:
      - 
      - 
      - wait_template: >
          {% set trigger = states('sensor.time')%}
          {{(now() - state_attr('script.play_sleep_radio','last_triggered')).total_seconds()
             > states('input_number.onsleep_delay')|int * 60}}
      - service: script.turn_off
        entity_id: script.play_sleep_radio

the wait_template itself is working fine (though boy checks per minute, and hence has a possible accuracy issue of a minute…) but the most important thing is to cancel the script itself.I’ve thought about wrapping this complete script in a ‘while’ and ‘until’ construction, but that only goes for loops?

the only other option I see now it an extra automation:

  - alias: Sleep radio off
    id: Sleep radio off
    trigger:
      platform: template
      value_template: >
        {% set trigger = states('sensor.time')%}
        {{(now() - state_attr('script.play_sleep_radio','last_triggered')).total_seconds()
           > states('input_number.onsleep_delay')|int * 60}}
    action:
      service: script.turn_off
      entity_id: script.play_sleep_radio

thanks for having a look?

Use a condition.

don’t think so, because where to put it?

this is the rest of the script:

  play_sleep_radio:
    alias: Play sleep radio
    mode: restart
    sequence:
      - condition: state
        entity_id: input_boolean.sleep_radio
        state: 'on'
      - service: media_player.volume_set
        data:
          entity_id: >
            {{states('sensor.sleep_radio')}}
          volume_level: >
            {{states('input_number.sleep_radio_volume')}}
      - service: media_player.play_media
        data:
          entity_id: >
            {{states('sensor.sleep_radio')}}
          media_content_id: >
            {{states('sensor.radio_station_sleep')}}
          media_content_type: music
      - delay: >
          {{states('input_number.decrease_volume_delay')|int}}
      - alias: Decrease volume AS LONG AS the conditions are true
        repeat:
          while:
            - condition: state
              entity_id:
                - input_boolean.sleep_radio
                - input_boolean.decrease_volume
              state: 'on'
            - >
                {{states(states('sensor.sleep_radio')) == 'playing'}}
            - >
                {{states('sensor.sleep_radio_volume')|float > 0}}
          sequence:
            - service: media_player.volume_set
              data:
                entity_id: >
                  {{states('sensor.sleep_radio')}}
                volume_level: >
                  {% set level =
                     states('sensor.sleep_radio_volume')|float -
                     states('input_number.decrease_volume')|float %}
                  {% if level > 0.05 %} {{level|round(2)}}
                  {% else %} 0.01
                  {% endif %}
            - delay: >
                {{states('input_number.decrease_volume_delay')|int}}

there’s no spot to put a condition. This has to run to the end, then stop deceasing the volume, which it does now perfectly, and then if the on sleep_delay has expired, should stop (the script)

What’s wrong with the existing script?

put another service/condition after the loop…

nothing, but it is endless :wink: I want it to stop playing after the onsleep_delay has expired and I probably will have fallen asleep already…

If you’re looping endlessly then you need put the condition in the while

well, the issue is, it should be able to decrease first, and then keep playing until the sleep_delay has expired. If I put the condition in the while, it will never do anything unless the while loop takes longer than the sleep_delay. Which is probably never the case, since it would be a matter of minutes before it reaches the minimum volume, and then should play on. And on.

until the sleep_delay expires.

btw, I’ve discovered the script also updates its last_triggered attribute upon the action in the loop, so if I set that to 30 sec, I can leave out the sensor.time in the trigger :wink:

notice the minute difference…

anyways, ive only been able to stop the script using the extra automation, which, as ive noticed, also needs to stop the media_player, because simply stopping the script doesnt do that. Needs to be this now:

  - alias: Sleep radio off
    id: Sleep radio off
    trigger:
      - platform: template
        value_template: >
          {% set trigger = states('sensor.time')%}
          {{(now() - state_attr('script.play_sleep_radio','last_triggered')).total_seconds()
             > states('input_number.sleep_delay')|int * 60}}
      - platform: state
        entity_id:
          - input_boolean.sleep_radio
          - input_boolean.sleepclock_enabled
          - script.play_sleep_radio
        to: 'off'
    action:
      - service: script.turn_off
        entity_id: script.play_sleep_radio
      - service: media_player.turn_off
        data:
          entity_id: >
            {{states('sensor.sleep_radio')}}

would be cool if we had a service until: in the script syntax, we could call without a while:, securing a script would always finish, even after the loop had finished (which doesn’t necessarily mean the script finishes)

yeah, so put a condition in the while loop that checks the current delay duration against the total sleep_delay.

{{ repeat.index * states(‘input_number.decrease_volume_delay’)|int <= states(‘input_number.sleep_delay’)|int }}

wait that’s new for me, what does this do? do we have documentation on this? this isn’t much to go on

just to be sure: even without the loop I would want this to be happening. I can’t see why this should be part of the loop, because that’s only the beginning of the script.

I really think I like what you suggest here, and have other uses for that, if I understand it correctly. but this exact spot, I am uncertain it is the correct technique.

This is simple coding of a while statement. Remember every time i’ve said “Hey you should do a beginner coding class” and you brush me off? These classes teach that stuff. Those documents give you plenty of information to do what I did. All I’m doing is mulitplying your incremental duration multiplied by the current increment and checking it against the total time. Simple math. The rest is knowing what a while loop is.

please try to understand me, I do know what the while loop is, and I understand what you do, multiplying the incremental duration. I can add that condition for sure, to catch the situation the onsleep_delay is shorter than the while loop.

But, what I am trying to say is that it is most likely that comparison is not very helpful, as eg at the 5th loop run (*30 seconds = 150 seconds, the volume level can be 0.01, so the while loop finishes (and the onsleep_delay has not been expired at all, because that was set eg at 10 minutes)
After that, the media_player is playing at 0.01 volume level indefinitely.

What I want to happen is, that after that while loop has had its last run, the main script continues and finishes at the expiry of the sleep_delay (renamed it).

I just figured it shouldn’t stop the script itself, but issue a media_player.turn_off service, which then returns the script and finishes it, which would completely result in:

  play_sleep_radio:
    alias: Play sleep radio
    mode: restart
    sequence:
      - condition: state
        entity_id: input_boolean.sleep_radio
        state: 'on'
      - service: media_player.volume_set
        data:
          entity_id: >
            {{states('sensor.sleep_radio')}}
          volume_level: >
            {{states('input_number.sleep_radio_volume')}}
      - service: media_player.play_media
        data:
          entity_id: >
            {{states('sensor.sleep_radio')}}
          media_content_id: >
            {{states('sensor.radio_station_sleep')}}
          media_content_type: music
      - delay: >
          {{states('input_number.decrease_volume_delay')|int}}
      - alias: Decrease volume AS LONG AS the conditions are true
        repeat:
          while:
            - condition: state
              entity_id:
                - input_boolean.sleep_radio
                - input_boolean.decrease_volume
              state: 'on'
            - >
                {{states(states('sensor.sleep_radio')) == 'playing'}}
            - >
                {{states('sensor.sleep_radio_volume')|float > 0.01}}
            - >
                {{repeat.index * states('input_number.decrease_volume_delay')|int <=
                  states('input_number.sleep_delay')|int * 60}}
          sequence:
            - service: media_player.volume_set
              data:
                entity_id: >
                  {{states('sensor.sleep_radio')}}
                volume_level: >
                  {% set level =
                     states('sensor.sleep_radio_volume')|float -
                     states('input_number.decrease_volume')|float %}
                  {% if level > 0.05 %} {{level|round(2)}}
                  {% else %} 0.01
                  {% endif %}
            - delay: >
                {{states('input_number.decrease_volume_delay')|int}}
      - wait_template: >
          {% set trigger = states('sensor.time')%}
          {{(now() - state_attr('script.play_sleep_radio','last_triggered')).total_seconds()
             > states('input_number.sleep_delay')|int * 60}}
      - service: media_player.turn_off
        data:
          entity_id: >
            {{states('sensor.sleep_radio')}}

btw, I’ve never brushed you off, would hate to think you thought I did.

make a variable in your sequence equal to the number of loops multiplied by your incremental duration. Then add a delay using said variable to wait the remaining time.

like this:

      - alias: Decrease volume AS LONG AS the conditions are true
        repeat:
          while:
            - condition: state
              entity_id:
                - input_boolean.sleep_radio
                - input_boolean.decrease_volume
              state: 'on'
            - >
              {{states(states('sensor.sleep_radio')) == 'playing'}}
            - >
              {{states('sensor.sleep_radio_volume')|float > 0.01}}
            - >
              {{repeat.index * states('input_number.decrease_volume_delay')|int <=
                states('input_number.sleep_delay')|int * 60}}
          sequence:
            - variables:
                loop_duration: >
                  {{repeat.index * states('input_number.decrease_volume_delay')|int}}
            - service: media_player.volume_set
              data:
                entity_id: >
                  {{states('sensor.sleep_radio')}}
                volume_level: >
                  {% set level =
                     states('sensor.sleep_radio_volume')|float -
                     states('input_number.decrease_volume')|float %}
                  {% if level > 0.05 %} {{level|round(2)}}
                  {% else %} 0.01
                  {% endif %}
            - delay: >
                {{states('input_number.decrease_volume_delay')|int}}
      - condition: template
        value_template: >
          {{states(states('sensor.sleep_radio')) == 'playing'}}
      - delay: >
          {% set max_playing_time = states('input_number.sleep_delay')|int * 60 %}
          {{max_playing_time - variables.loop_duration}}
#      - wait_template: >
#          {% set trigger = states('sensor.time')%}
#          {{(now() - state_attr('script.play_sleep_radio','last_triggered')).total_seconds()
#             > states('input_number.sleep_delay')|int * 60}}
      - service: media_player.turn_off
        data:
          entity_id: >
            {{states('sensor.sleep_radio')}}

need to check if this needs to explicitly add the initial first play time explicitly ? I mean, it only starts looping given amount of time after the full script has started? The repeat.index is 1 loop behind per definition.

meaning the delay should probably be:

      - delay: >
          {% set max_playing_time = states('input_number.sleep_delay')|int * 60 %}
          {{max_playing_time - variables.loop_duration - states('input_number.decrease_volume_delay')|int}}

though, no matter which I choose, the script stops after 150 seconds. Even if I use this in the wait_template:

{{(as_local(states.sensor.time.last_updated) - state_attr('script.play_sleep_radio','last_triggered')).total_seconds()
             > states('input_number.sleep_delay')|int * 60}}

it should be seen correctly, a this is in the template editor:

restarting that before sensor, time has updated has its own peculiarities:

ok, a separate post for clarity. I had an error in the condition above and edited that to

{{states(states('sensor.sleep_radio')) == 'playing'}}

so now the script passes that correctly.

I can only get it to function properly using the wait_template, and all of the variables and delay using that variable commented.

secondly, somehow the script itself doesnt keep its last_triggered, nor last_changed after the script had ended, because then there’s always this in the log:

TypeError: unsupported operand type(s) for -: 'datetime.datetime' and 'NoneType'

using either:

          {% set trigger = states('sensor.time')%}
          {{(now() - state_attr('script.play_sleep_radio','last_triggered')).total_seconds()
             > states('input_number.sleep_delay')|int * 60}}

or

          {{(as_local(states.sensor.time.last_changed) - state_attr('script.play_sleep_radio','last_triggered')).total_seconds()
             > states('input_number.sleep_delay')|int * 60}}

the latter I dont want to use because it updates not frequently enough, but still, this is a template used frequently, so I dont see why it wont work now, until checking the template editor:

note that when the script is started, the last_triggered is noted correctly.


what am I missing here??

getting back on the variable:
if I declare it like this:

 sequence:
            - variables:
                loop_duration: >
                  {{repeat.index * states('input_number.decrease_volume_delay')|int}}

do I call it in the template after the while like this?:

      - delay: >
          {% set max_playing_time = states('input_number.sleep_delay')|int * 60 %}
          {{max_playing_time - variables.loop_duration}}

or is it variable.loop_duration, or plain loop_duration after all?. Really sorry but since this is not documented, and eg button-card uses a comparable syntax, I need to ask. Especially since this:

      - delay: >
          {% set max_playing_time = states('input_number.sleep_delay')|int * 60 %}
          {{max_playing_time - loop_duration - states('input_number.decrease_volume_delay')|int}}

seems not to be correct, even while using loop_duration, following the 115 release notes

that needs to be cast as an int

I was missing the reload of the integration Script, causing all scripts to lose their historic data…

thanks, I’ll try and report back.

ok reporting back:
seems no matter what I use for the variable in the template, the error is always reported:

Play sleep radio: Error rendering Play sleep radio delay template: UndefinedError: 'variables' is undefined

in various forms, depending on whether I use:

 {{max_playing_time - variables.loop_duration|int - states('input_number.decrease_volume_delay')|int}}

or variable.loop_duration|int or loop_duration|int .

Must be because it is used outside the loop of the while script then.

So, I have to revert to my original use of the wait_template. Which caused the error of the unsupported operand. Adding the |default(0) filter there helps:

      - wait_template: >
          {% set updater = states('sensor.time') %}
          {{(now() - state_attr('script.play_sleep_radio','last_triggered')|default(0)).total_seconds()
             > states('input_number.sleep_delay')|int * 60}}

and, I can even us that in a template trigger for an automation, that stops the script on the other triggers. So cutting a few lines again.

What I have been wondering, is using a faster trigger in the template for updater than sensor.time. I have a sensor that updates on all energy state hanges in the system, which is way more frequent. Is there an easy way to determine the most updates sensor in the system? What template could we use to find that?

can make an empty automation and use that…

  - alias: Trigger per 2 seconds
    id: Trigger per 2 seconds
    trigger:
      platform: time_pattern
      seconds: '/2'
    action:
      delay:
        seconds: 0

to trigger

        {% set updater = states('automation.trigger_per_2_seconds') %}
          {{(now() - state_attr('script.play_sleep_radio','last_triggered')|default(0)).total_seconds()}}

update:
@amelchio suggested this:

         {% set delay = states('input_number.sleep_delay')|int * 60 %}
         {% set spent = (now() - state_attr('script.play_sleep_radio','last_triggered')|default(0)).total_seconds() %}
         {{ [0, delay-spent] | max }}

which works, without having the need for an updating entity_id at all…
nice.