How to loop play sound files [Solved]

Before diving too deep, confirm the automations are all ‘on’ and haven’t somehow been (inexplicably) turned ‘off’.

If they are on then first thing to check is if ‘Looper controller’ is being triggered because that’s what gets the whole thing going. If it is being triggered then the next possible source of failure is it doesn’t start the timer with a duration > 0.

I think I may have found the problem.

You defined the timer with an initial duration using this format: '00:00:20'

I recall while experimenting with timers that it will now expect its duration to always be supplied in that format. So the template has to convert integer seconds into the hours:minutes:seconds format.

{{ x if trigger.to_state.state == 'on' else 0 | timestamp_custom('%H:%M:%S', false) }}

I havent done that yet, I started off with your code. didn’t work. Ive now set the initial duration: no difference.
Will check now with your timestamp

and yes the automatons are on, but I will set initial_state to be sure…

well, nothing happening . a mystery. especially since controller and finished have both been triggered, but the started has never been triggered :wink:

while the timer is idle:


note the duration set to 00:00:02, which is the actual duration of the sound_bite

changed the template to:

      {% set x = state_attr(states('sensor.sound_bite_player'),'media_duration')|round|int %}
      {{ (x if states('input_boolean.loop_sound_bite_timer') == 'on' 
           else 0 )| timestamp_custom('%H:%M:%S', false) }}

because otherwise only the 0 (off state) would get the correct timestamp.

  • You’re saying ‘Looping controller’ and ‘Looper timer finished’ have been triggered.
  • The indication of 00:00:02 suggests ‘Looping controller’ did its job and called timer.start service with duration 2 seconds.

What you’re saying is there is no evidence that the timer actually started. In other words, the sound bite didn’t play.

I’m not sure how to interpret these results. If timer.start is called with duration: 2 then it must have started the timer. It produces a timer.started event which would trigger ‘Looper timer started’. :thinking::man_shrugging:

As an experiment, replace the action in ‘Looper timer started’ with something simpler like toggling some other input_boolean. If it fails to do that then we’ve ruled out the possibility it’s somehow related to operating a media_player.

ok i will.

but isn’t this odd:


timer is active, but the remaining duration stays 2 seconds…?

btw, I can take out the media_player bit but if I trigger the manually, it plays correctly, so the action part is correct, its the trigger that’s not working as expected… all seems fine:

21
16
cut of the last line since it reveals my duckdns settings :slight_smile:

and changing the service made no difference, nothing happening, the 2nd automation doesn’t get triggered.

I wish I could remember all the results of my own timer experiments! I think that would be normal if the timer were canceled with the timer.cancel service. The timer’s countdown is interrupted and the duration is set to its last known initial value. It may also explain why it indicates the timer is active as opposed to idle (which is its normal ‘I am not currently doing anything’ state).

this is getting rather interesting. Ive entered some notifications as I always do when stepping through new automatons and their various changes (btw changed some templates to be interpretable by the dev-template too:

  - alias: 'Looping controller'
    id: 'Looping controller'
    initial_state: 'on'
    trigger:
      platform: state
      entity_id: input_boolean.loop_sound_bite_timer
    condition: []
    action:
      - service_template: >
          {% if is_state('input_boolean.loop_sound_bite_timer','on') %} timer.start
          {% else %} timer.finish
          {% endif %}
#        {{'timer.start' if states('input_boolean.loop_sound_bite_timer') == 'on' else 'timer.finish'}}
        data_template:
          entity_id: timer.looper
          duration: >
            {% set x = state_attr(states('sensor.sound_bite_player'),'media_duration')|round|int %}
            {{ (x if states('input_boolean.loop_sound_bite_timer') == 'on' 
                 else 0 )| timestamp_custom('%H:%M:%S', false) }}
      - service: notify.marijn
        data:
          message: boolean changed state
# The second automation is triggered when the timer starts. It’s job is to simply start playing the media.

  - alias: 'Looper timer started'
    id: 'Looping timer started'
    initial_state: 'on'
    trigger:
      platform: event
      event_type: timer.started
      event_data:
        entity_id: timer.looper
    condition: []
    action:
      - service: notify.marijn
        data:
          message: action because timer started
      - service: script.sound_bite

# The third and final automation is triggered when the timer finishes. It’s job is to restart the timer but 
# only if input_boolean.loop_sound_bite is on.

  - alias: 'Looper timer finished'
    id: 'Looping timer finished'
    initial_state: 'on'
    trigger:
      platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.looper
    condition:
      condition: template
      value_template: >
        {{is_state('input_boolean.loop_sound_bite_timer','on')}}
    action:
      - service: timer.start
        entity_id: timer.looper
      - service: notify.marijn
        data:
          message: action because timer finished

all that I get is the opening notification boolean has changed, and then action because timer finished keeps coming. no intermediary ‘action because timer started’ ! triggering it manually plays the file, and give me the notification. so the yaml is alright.

also, when I turn-off the boolean again, I get no message, which is surprising isn’t it, since it should have been triggered

It seems to behave like it doesn’t understand the timer.started event.

Maybe it’s an irrelevant question but what is the version of Home Assistant you are using?

Version 0.85 introduced new timer events:

Added events STARTED, RESTARTED AND PAUSED (@mjrider - #19516) (timer docs)

bingo i guess, this is 84.3…

now what are the old ones… Ill dive into this later on

though they seem to be available?

installed it on my 90.2 instance and it seems to be working!
One big issue though, the media_duration isn’t set in this sequence, so it takes the duration (if at all, if a fine has not been played yet, the attribute isn’t set yet, as mentioned earlier) of the prvious file.

In my other setup, this only happens the first time, and in the rest of the looping, attribute is set correctly, and looping is great.

in the timer setup this doesn’t happen and the timing stays determined by the previous file, played outside this looping sequence.

ive tried to add the script.sound_bite (which plays the file once, and thus should set its duration attribute) and see if that would set the duration, but it obviously doesn’t…dont now why yet.

secondly: I still don’t get a notification when the boolean is turned_off. Could this have to do with the fact that there’s a timer duration being set, while the the timer is turned_off? maybe that isn’t as it should be? And we should change that template. According to the docs, the entity_id should be inline with the service: so maybe this?:

      - service_template: >
          {% if is_state('input_boolean.loop_sound_bite_timer','on') %} timer.start
          {% else %} timer.finish
          {% endif %}
        entity_id: timer.looper
        data_template:
          duration: >
            {% set x = state_attr(states('sensor.sound_bite_player'),'media_duration')|round|int %}
            {{ x | timestamp_custom('%H:%M:%S', false) }}

If I enter the current automation in dev-template it does show correctly:

using this:


  - alias: 'Looping controller'
    id: 'Looping controller'
    initial_state: 'on'
    trigger:
      platform: state
      entity_id: input_boolean.loop_sound_bite_timer
    condition: []
    action:
# have the soundfile played once to set the 'media_duration' attribute
      - service_template: >
          {% if is_state('input_boolean.loop_sound_bite_timer','on') %} script.sound_bite
          {% else %} script.dummy
          {% endif %}
      - service_template: >
          {% if is_state('input_boolean.loop_sound_bite_timer','on') %} timer.start
          {% else %} timer.finish
          {% endif %}
#        {{'timer.start' if states('input_boolean.loop_sound_bite_timer') == 'on' else 'timer.finish'}}
        data_template:
          entity_id: timer.looper
          duration: >
            {% set x = state_attr(states('sensor.sound_bite_player'),'media_duration')|round|int %}
            {{ (x if states('input_boolean.loop_sound_bite_timer') == 'on' 
                 else 0 )| timestamp_custom('%H:%M:%S', false) }}
      - condition: template
        value_template: >
          {{is_state('input_boolean.notify_developing','on')}}
      - service: notify.marijn
        data_template:
          message: >
            Loop boolean changed to {{states('input_boolean.loop_sound_bite_timer')}}

concerning the second issue of the boolean not triggering the ‘off’ state, Ive tested with this dummy automation, and that works as expected:

  - alias: 'Looping controller dummy'
    id: 'Looping controller dummy'
    initial_state: 'on'
    trigger:
      platform: state
      entity_id: input_boolean.loop_sound_bite_timer
    condition: []
    action:
# have the soundfile played once to set the 'media_duration' attribute
      - service_template: >
          {% if is_state('input_boolean.loop_sound_bite_timer','on') %} script.dummy
          {% else %} script.dummy
          {% endif %}
      - service_template: >
          {% if is_state('input_boolean.loop_sound_bite_timer','on') %} timer.start
          {% else %} timer.finish
          {% endif %}
        entity_id: timer.looper_dummy
      - condition: template
        value_template: >
          {{is_state('input_boolean.notify_developing','on')}}
      - service: notify.marijn
        data_template:
          message: >
            Looper dummy boolean changed to {{states('input_boolean.loop_sound_bite_timer')}}

so that also points to the action part not being as it should.

Just timer.finished and timer.cancelled. Here’s the original documentation page.

That’s why I said my solution is no longer worth considering unless the sound file’s duration can be determined at the moment the timer is started (and has its duration set).

Works for me.

Anyway, an interesting exercise but an unusable solution for you given that:

  1. You’re using an old version of Home Assistant that doesn’t support timer events that were introduced in 0.85.
  2. The sound file’s duration must be available at the moment the timer is started.

would it be worth the try to use

trigger:
  platform: state
  entity_id: timer.loop
  to: 'active'

as a replacement for the event: started on my Ha 84.3 system? It works fine on my HA 90.2 system

Maybe, as an academic exercise. However, it still leaves the challenge of acquiring the sound bite’s duration, for setting the timer’s duration, at the moment the timer is started.

this seems to do the trick. May difference with the previous setup is taking out the service_template for starting/finishing the timer (with its associated timing template.

although I have set an initial delay after the script.sound_bite, this still is somewhat insecure. It does play correctly from the second run onwards. And it does notify me when the boolean is flipped off finally…


script:
  looping_controller_on:
    alias: Looping controller on
    sequence:
      service: timer.start
      data_template:
        entity_id: timer.looper
        duration: >
          {% set x = state_attr(states('sensor.sound_bite_player'),'media_duration')|round|int %}
          {{ x }}

  looping_controller_off:
    alias: Looping controller off
    sequence:
      service: timer.finish
      entity_id: timer.looper

automation:
# The first automation is triggered by input_boolean.loop_sound_bite_timer and simply starts/stops the timer 
# (i.e. starts/stops looping).

  - alias: 'Looping controller'
    id: 'Looping controller'
    initial_state: 'on'
    trigger:
      platform: state
      entity_id: input_boolean.loop_sound_bite_timer
    condition: []
    action:
# have the soundfile played once to set the 'media_duration' attribute
      - service_template: >
          {% if trigger.to_state.state == 'on' %} script.sound_bite
          {% else %} script.dummy
          {% endif %}
      - delay:
          seconds: >
            {% if trigger.to_state.state == 'on' %}
            {{1+state_attr(states('sensor.sound_bite_player'),'media_duration')|int}}
            {% else %} 0
            {% endif %}
      - service_template: >
          script.looping_controller_{{trigger.to_state.state}}
      - condition: template
        value_template: >
          {{is_state('input_boolean.notify_developing','on')}}
      - service: notify.marijn
        data_template:
          message: >
            Loop boolean changed to {{trigger.to_state.state)}}

# The second automation is triggered when the timer starts. It’s job is to simply start playing the media.

  - alias: 'Looper timer started'
    id: 'Looping timer started'
    initial_state: 'on'
    trigger:
      platform: event
      event_type: timer.started
      event_data:
        entity_id: timer.looper
    condition: []
    action:
      - service: script.sound_bite
      - condition: template
        value_template: >
          {{is_state('input_boolean.notify_developing','on')}}
      - service: notify.marijn
        data:
          message: 'Action because timer started: play file'

# The third and final automation is triggered when the timer finishes. It’s job is to restart the timer but 
# only if input_boolean.loop_sound_bite is on.

  - alias: 'Looper timer finished'
    id: 'Looping timer finished'
    initial_state: 'on'
    trigger:
      platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.looper
    condition:
      condition: template
      value_template: >
        {{is_state('input_boolean.loop_sound_bite_timer','on')}}
    action:
      - service: timer.start
        entity_id: timer.looper
      - condition: template
        value_template: >
          {{is_state('input_boolean.notify_developing','on')}}
      - service: notify.marijn
        data:
          message: Loop action because timer finished

Ouch! Automation–> Frankenmation!

You may wish to amend this comment line:
# The first automation is triggered by input_boolean.loop_sound_bite_timer and simply starts/stops the timer
because it certainly no longer ‘simply starts/stops the timer’! :wink:

Out of curiosity how is sensor.sound_bite_player defined? I’d like to see how you acquire the sound bite’s duration and assign it to the media_duration attribute.

Lol… well, it was all about the somewhat contrived template that tried to either start or finish the timer, with the templated time. Taking that apart solved it. So, yes, in fact it is much simpler now than it was before… :wink:

      sound_bite_player:
        friendly_name: Sound bite player
        value_template: >
          {% set state = states('input_select.sound_bite_player') %}
          {% if state in ['Woonkamer','Hobbykamer','Hall','Master bedroom','Office'] %} media_player.googlehome_{{(state)|lower|replace(' ','_')}}
          {% elif state == 'Gym' %} media_player.chromecastaudio_gym
          {% else %} group.broadcast
          {% endif %}

      sound_bite:
        friendly_name: Sound bite
        value_template: >
            {% set state = states('input_select.sound_bite') %}
            {% set url = states('input_text.base_url') %}
            {% set path = '/local/sounds/sound_bites/'%}
            {% set sound_bite = state|lower|replace(' ','_') %}
            {% set ext = '.mp3' %}
            {{[url,path,sound_bite,ext]|join}}

Haha! If this is ‘contrived’:

{{'timer.start' if trigger.to_state.state == 'on' else 'timer.finish'}}

then some of your templates that I’ve seen are positively Byzantine! :wink:

Regarding the sensor configurations you provided, I’d like to see how the media_duration attribute is set but it’s not shown. What sets it?

not exactly sure what you mean, but when playing a file the media_player calculates this automatically and it is set as one of the attributes of the media_player

I know it would have been simpler to use the media_players entity_id directly, but since this player is selected out of a number of players, I needed to template this.

Haha, no I was talking about this one:

    action:
      - service_template: >
          {% if is_state('input_boolean.loop_sound_bite_timer','on') %} timer.start
          {% else %} timer.finish
          {% endif %}
#        {{'timer.start' if states('input_boolean.loop_sound_bite_timer') == 'on' else 'timer.finish'}}
        data_template:
          entity_id: timer.looper
          duration: >
            {% set x = state_attr(states('sensor.sound_bite_player'),'media_duration')|round|int %}
            {{ (x if states('input_boolean.loop_sound_bite_timer') == 'on' 
                 else 0 )| timestamp_custom('%H:%M:%S', false) }}

of which the ‘off’ option apparently caused thing to go awol.

The state of sensor.sound_bite_player is the name of a media_player entity, either:
media_player.googlehome_<room name>
or
media_player.chromecastaudio_gym.

So this template is getting media_duration from a media_player entity:

state_attr(states('sensor.sound_bite_player'),'media_duration')

As I’ve mentioned earlier, I don’t have experience with Home Assistant’s media_player component. However, it stands to reason that getting media_duration from a media_player implies some sort of media is already cued up for playback on the player. Otherwise, a media_player with no media cued to play would have its media_duration equal to 0.

So how is the media_player initialized with media?


EDIT
To clarify, the point I’m driving at is that maybe this automation should also set the media to be played by the media_player as opposed to assuming the player already has something cued for playback?

that’s whyI added this script:

  sound_bite:
    alias: Sound bite
    sequence:
      - service: media_player.play_media
        data_template:
          entity_id: >
            {{states('sensor.sound_bite_player')}}
          media_content_id: >
            {{states('sensor.sound_bite')}}
          media_content_type: 'music'