Help creating a template sensor to overcome Google Cast issue

Hello guys!

As many of you know, currently it is not possible to send an audio (like an announcement, for example) to a cast device (Google Home Mini in my case) and not cancel/stop the currently playing media. This is a pain since you have to resume playing manually the previous media.

I’ve came up with an idea that could overcome this problem: create a template_sensor that “calculates” all media_player entities that are currently not playing and result as text, so it can be used on any script or automation as the entity_id to play the desired media without interrupting the currently playing ones.

I was not able to came up with a reasonably good solution/code so I’ve decided to write this post. Maybe a good soul here could help me out? I think this could be a nice solution for many people as well.

The idea is to, firstly, see if all speakers are playing something (Group created on the app), if yes, check all other media_players on a custom list that is not currently playing anything and result all of them as text, comma separated. If non of them are playing, result the all speakers group entity_id, if all speakers (group) are playing, output nothing, null or empty, not sure.

I find very important to use the “all speakers” group because of the audio synchronization problem if all of them are played at the same time separately…

Here is my “all speakers” group:

media_player.casa_inteira

These are all available individual Google Home Minis:

media_player.banheiro_social
media_player.banheiro_suite
media_player.cozinha
media_player.quarto
media_player.sala
media_player.suite

For example:

if media_player.banheiro_social and media_player.cozinha are playing, the sensor_template should output:

media_player.banheiro_suite, media_player.quarto, media_player.sala, media_player.suite

This can be used to call only the available GHM at the time the service is called, so the rest are not interrupted.

This would be the service call:

- service_template: media_player.play_media
  data_template:
    entity_id: {{states.sensor.available_speacker.state}}
    media_content_id: 'http://hassio.local:8123/local/audio/tts/sistema_reiniciado.mp3'
    media_content_type: 'audio/mp3'

Does it make sense?

Sorry, english is not my mother language :slight_smile:

Have a great day guys!

Maybe @petro with his amazing knowledge on lists and variables could jump in? Thanks!

If I understand you correctly, you want to create a template sensor whose state contains a list of available (idle) media_players.

For example, the state of sensor.available_speakers can be:
media_player.banheiro_suite, media_player.quarto, media_player.sala, media_player.suite

I don’t think creating the template sensor presents a problem. However, I think consuming the template sensor’s state will be messy.

The template sensor’s state will be a string, not a list. For example, put this into the Template Editor and it will report: media_player.quarto

{% set x = 'media_player.banheiro_suite, media_player.quarto, media_player.sala, media_player.suite' %}
{{x.split(',')[1]}}

To use it in a data_template:

- service: media_player.play_media
  data_template:
    entity_id: >-
     {% set x = states('sensor.available_speaker') %}
     {{ x.split(',')[0] }}
    media_content_id: 'http://hassio.local:8123/local/audio/tts/sistema_reiniciado.mp3'
    media_content_type: 'audio/mp3'

That’s the bare minimum template … but it’s not good enough.

  • What if there are no available media_players? It should check for that … and then what should it assign to entity_id?
  • If there are several available media_players, which one should it use? Always the first one?

Hello @123! Thanks for your time and effort in helping me! I really appreciate it.

Answering your questions:

  1. This is what I am not sure on how to do it. It should output something like null, something that does not crashes the script or result in a halt.
  2. The idea is to play the media on ALL available speakers. Let’s say my wife is listening to a music on the main bathroom, all other speakers should play the media (mostly TTS announcements in my case).

The idea of a template sensor always showing the currently available speakers in real time is that I don’t need to use this script on all media_player.play_media service calls, just use the current state of this sensor on the entity_id part.

The idea of using all entities between commas should work as one single string, I use this all the time to call many entities over one single string.

I admit that I don’t know what that magic value would be. Normally when you call media_player.play_media you’re expected to provide a valid entity_id.

That presents yet another interesting challenge because the template sensor’s state is a string, not a list. In your example you did this:

entity_id: {{states.sensor.available_speacker.state}}

That won’t work because state’s value isn’t a list.

Based on results from other posts (that also needed to create lists), you can’t use a template to produce a list. It may look like it creates a list, but it’s a string.

The solution might be to call a python_script and pass it the template sensor’s state (and media_content_id and media_content_type). A python_script offers more flexibility.

For example:

  • If state contains no media_players (an empty string), the python_script would simple not call media_player.play_media.
  • If state does contain media_players, the python_script can parse the string and loop through all available media_players (calling media_player.play_media in each loop).

EDIT
Ignore what I said about the list …

Hello again! Thanks for your feedback.

I’ve just tested this and it works just fine:

- service: media_player.play_media
  data:
    entity_id: media_player.banheiro_suite, media_player.quarto, media_player.sala, media_player.suite
    media_content_id: 'http://hassio.local:8123/local/audio/tts/sistema_reiniciado.mp3'
    media_content_type: 'audio/mp3'

I have many scripts that uses these single lines strings with multiple entity ids separated by commas.

A few weeks ago, I was trying to code a script that only results on a string of entities, separated by commas, of lights that were currently on. A great friend here came up with this:

- service_template: >-
    {% set lights = ['light.area_servico','light.suite_banheiro_principal','light.suite_banheiro_espelho','light.corredor','light.cozinha','light.entrada','light.ledchaocozinha','light.ledcozinhabalcao','light.ledsaladesktop','light.ledsalatv','light.ledsuitecama','light.ledsuitetv','light.mesa_de_jantar','light.sala_principal','light.suite_principal','light.status_maquina_de_lavar_loucas','light.movel_da_sala'] %}
    {% for item in lights if states(item) == 'on' %}
    {% if loop.first %}
      light.turn_off
    {% endif %}
    {% else %}
      script.nada
    {% endfor %}
  data_template:
    entity_id: >-
      {% set entities = ['light.area_servico','light.suite_banheiro_principal','light.suite_banheiro_espelho','light.corredor','light.cozinha','light.entrada','light.ledchaocozinha','light.ledcozinhabalcao','light.ledsaladesktop','light.ledsalatv','light.ledsuitecama','light.ledsuitetv','light.mesa_de_jantar','light.sala_principal','light.suite_principal','light.status_maquina_de_lavar_loucas','light.movel_da_sala'] %}
      {{ states.light | selectattr('entity_id', 'in', entities) | selectattr('state','eq','on') | map(attribute='entity_id') | join(', ') }}

This works flawlessly!

I was thinking of doing something similar but with a template_sensor so I could use it on all of mines media player service calls.

I do as well … and I think I need to get some sleep because I can’t believe I suggested it wouldn’t work! Of course it works. My mistake! :man_shrugging:

I also proved it to myself by creating a template sensor representing all lights currently on. I then used its state in an automation to turn off all lights that are on. It worked perfectly … and made the house quite dark.

  - platform: template
    sensors:
      lights_on:
        value_template: "{{ states.light | selectattr('state','eq','on') | map(attribute='entity_id') | join(', ') }}"

The remaining challenge is what to do if there are no available media_players. I confirmed it cannot be an empty string. When the service is executed, Home Assistant reports an error:

Invalid data for call_service at pos 1: not a valid value for dictionary value @ data['entity_id']

However, when I set it to non-existent entity (light.none) it did not report an error. So that’s a possible solution. If no media_players are available, the template should report a non-existent entity like media_player.none.

So this template should handle the entity_id:

{% set x = states('sensor.available_players') %}
{{ x if x | length > 0 else 'media_player.none'}}

Now the remaining challenge is to create the template sensor. I’m at a bit of a disadvantage because I have no media_players to experiment with. I imagine the value_template will look something like this:

{{ states.media_player | selectattr('state','eq','idle') | map(attribute='entity_id') | join(', ') 

You’ll have to test it and see if it works. I don’t know which attribute indicates when the media_player is available. I chose state = idle but it might be something else


  - platform: template
    sensors:
      available_players:
        entity_id:
          - media_player.banheiro_social
          - media_player.banheiro_suite
          - media_player.cozinha
          - media_player.quarto
          - media_player.sala
          - media_player.suite
        value_template: "{{ states.media_player | selectattr('state','eq','idle') | map(attribute='entity_id') | join(', ') }}"

The service section of an automation:

- service: media_player.play_media
  data_template:
    entity_id: >-
      {% set x = states('sensor.available_players') %}
      {{ x if x | length > 0 else 'media_player.none'}}
    media_content_id: 'http://hassio.local:8123/local/audio/tts/sistema_reiniciado.mp3'
    media_content_type: 'audio/mp3'
1 Like

I wouldn’t use a template sensor because it has a limit of 255 characters. I’d just calculate it on the fly inside the service. I’d also entertain the idea of using != ‘playing’. This may be unwanted if you don’t want to deal with it when paused or stopped. idle may be the way to go.

I’d also skip doing the media_player.none stuff by adding a condition. It would be a little long winded but you should never have to update it and no errors should get produced.

- condition: template
  value_template: >-
      {% set media_players = states.media_player 
          | selectattr('entity_id', 'in', state_attr('media_player.casa_inteira', 'entity_id')) 
          | selectattr('state','!=','playing') 
          | map(attribute='entity_id') 
          | join(', ') %}
      {{ media_players | length > 0 }}
- service: media_player.play_media
  data_template:
    entity_id: >-
      {{ states.media_player 
          | selectattr('entity_id', 'in', state_attr('media_player.casa_inteira', 'entity_id')) 
          | selectattr('state','!=','playing') 
          | map(attribute='entity_id') 
          | join(', ') }}
    media_content_id: 'http://hassio.local:8123/local/audio/tts/sistema_reiniciado.mp3'
    media_content_type: 'audio/mp3'
1 Like

Hello my friend! Thanks for passing by, I really appreciate it.

The idea of using a template_sensor is that I would not have to use the same code over and over again, since I have about 20 scripts that involves a TTS announcement using the media_player.play_media service.

I’ve calculated that if all media players are selected on the output text, no more than 140 characters are used so I think the 255 limit would not be a problem, I believe.

Also, using a condition would halt the rest of the current running script, since many of them have other services calls after the media_player.play_media service.

Another important thing is that the code should always use the “all speakers group” called media_player.casa_inteira if all cast devices are idle, to make sure there are no audio sync problems when the media is played on all devices at the same time.

The use of != 'playing' sounds awesome! I really like it too.

I am still not sure what to do if there are no speakers available or media_player.casa_inteira is playing at the moment, resulting something like media_player.none. I don’t care if there are errors messages on the log about the lack of an entity_id on that service call if the script does not halt of anything bad happens…

Thanks a lot @petro!

Hahaha no worries my friend! It happens to the best of us :slight_smile:

Thanks for your code, it really helped on this project.

1 Like

@petro, I was thinking: is it possible to create a script just for that usage? A condition could be use to check if there are any speakers available (like you suggested before) and just pass the data part to the script? I’ve read somewhere that variables could be passed to another script when the service is called. Thanks.

Go to the Services page and enter:

  • Service: media_player.play_media
  • Entity: media_player.none

Click CALL SERVICE and check the log. There will be no WARNING or ERROR message concerning the non-existent entity.

Maybe a future version of Home Assistant will complain but it doesn’t in 0.91.

1 Like

Hello! Thanks for your suggestion.

I believe that @petro idea of using a script with a condition would be a better solution, since we can pass variables to the script, like this:

- service: script.ghm_play_media
  data_template:
    media_content_id: 'http://hassio.local:8123/local/audio/tts/sistema_reiniciado.mp3'

So it can call the script.ghm_play_media and pass the variable media_content_id or any TTS service data to a script like this:

ghm_play_media:
  sequence:
    - condition: template
      value_template: >-
          {% set media_players = states.media_player 
              | selectattr('entity_id', 'in', state_attr('media_player.casa_inteira', 'entity_id')) 
              | selectattr('state','!=','playing') 
              | map(attribute='entity_id') 
              | join(', ') %}
          {{ media_players | length > 0 }}
    - service: media_player.play_media
      data_template:
        entity_id: >-
          {{ states.media_player 
              | selectattr('entity_id', 'in', state_attr('media_player.casa_inteira', 'entity_id')) 
              | selectattr('state','!=','playing') 
              | map(attribute='entity_id') 
              | join(', ') }}
        media_content_type: 'audio/mp3'

This idea sounds great since if there are no speakers available, the condition would be met and the service would not be called at all.

I am still not sure if this would be solved:

Another important thing is that the code should always use the “all speakers group” called media_player.casa_inteira if all cast devices are idle , to make sure there are no audio sync problems when the media is played on all devices at the same time.

That would be very important as well.

Thanks!

Hello guys!

So here is the solution I’ve found

It is not the best way to do it, but it works:

## Scripts ##

ghm_play_media:
  sequence:
    - condition: template
      value_template: "{{ not is_state('media_player.casa_inteira' , 'playing')}}"
    - service: media_player.volume_set
      data_template:
        entity_id: >-
            {%- set players = ['media_player.banheiro_social', 'media_player.banheiro_da_suite', 'media_player.cozinha', 'media_player.quarto', 'media_player.sala', 'media_player.suite'] %}
            {{ states.media_player | selectattr('state','!=','playing') | selectattr('entity_id', 'in', players) | map(attribute='entity_id') | join(', ') }}
        volume_level: '{{volume}}'
    - service: media_player.play_media
      data_template:
        entity_id: >-
          {%- if is_state('input_boolean.boa_noite', 'on') %}
              {%- set players = ['media_player.banheiro_social', 'media_player.cozinha', 'media_player.quarto', 'media_player.sala'] %}
              {{ states.media_player | selectattr('state','!=','playing') | selectattr('entity_id', 'in', players) | map(attribute='entity_id') | join(', ') }}
          {%- else %}
              {%- if states.media_player.banheiro_social.state != 'playing' and
                    states.media_player.banheiro_suite.state != 'playing' and
                    states.media_player.cozinha.state != 'playing' and
                    states.media_player.quarto.state != 'playing' and
                    states.media_player.sala.state != 'playing' and
                    states.media_player.suite.state != 'playing' %}
                  media_player.casa_inteira
              {%- else %}
                  {%- set players = ['media_player.banheiro_social', 'media_player.banheiro_da_suite', 'media_player.cozinha', 'media_player.quarto', 'media_player.sala', 'media_player.suite'] %}
                  {{ states.media_player | selectattr('state','!=','playing') | selectattr('entity_id', 'in', players) | map(attribute='entity_id') | join(', ') }}
              {%- endif %}
          {%- endif %}
        media_content_type: 'audio/mp3'
        media_content_id: '{{media}}'

ghm_tts:
  sequence:
    - condition: template
      value_template: "{{ not is_state('media_player.casa_inteira' , 'playing')}}"
    - service: media_player.volume_set
      data_template:
        entity_id: >-
            {%- set players = ['media_player.banheiro_social', 'media_player.banheiro_da_suite', 'media_player.cozinha', 'media_player.quarto', 'media_player.sala', 'media_player.suite'] %}
            {{ states.media_player | selectattr('state','!=','playing') | selectattr('entity_id', 'in', players) | map(attribute='entity_id') | join(', ') }}
        volume_level: '{{volume}}'
    - service: tts.amazon_polly_say
      data_template:
        entity_id: >-
          {%- if is_state('input_boolean.boa_noite', 'on') %}
              {%- set players = ['media_player.banheiro_social', 'media_player.cozinha', 'media_player.quarto', 'media_player.sala'] %}
              {{ states.media_player | selectattr('state','!=','playing') | selectattr('entity_id', 'in', players) | map(attribute='entity_id') | join(', ') }}
          {%- else %}
              {%- if states.media_player.banheiro_social.state != 'playing' and
                    states.media_player.banheiro_suite.state != 'playing' and
                    states.media_player.cozinha.state != 'playing' and
                    states.media_player.quarto.state != 'playing' and
                    states.media_player.sala.state != 'playing' and
                    states.media_player.suite.state != 'playing' %}
                  media_player.casa_inteira
              {%- else %}
                  {%- set players = ['media_player.banheiro_social', 'media_player.banheiro_da_suite', 'media_player.cozinha', 'media_player.quarto', 'media_player.sala', 'media_player.suite'] %}
                  {{ states.media_player | selectattr('state','!=','playing') | selectattr('entity_id', 'in', players) | map(attribute='entity_id') | join(', ') }}
              {%- endif %}
          {%- endif %}
        message: '{{tts}}'

## Tests ##

ghm_play_media_test:
  sequence:
    - service: script.ghm_play_media
      data_template:
        media: 'http://hassio.local:8123/local/audio/tts/some_audio.mp3'
        volume: 0.5

ghm_tts_test:
  sequence:
    - service: script.ghm_tts
      data_template:
        tts: '<speak>Say something with Polly or Google format</speak>'
        volume: 0.5

It checks if all speakers group is playing something, if so, script does not start at all. If it is not playing, get only entity_id of devices not playing and pass the variables needed.

Also, I’ve added an “if” so it can determinate if someone has started the “good night” mode on the house (is_state('input_boolean.boa_noite', 'on')), so it only plays the media on devices outside the main bedroom.

Another cool feature is to pass the volume needed on each announcement/media play.

I will set this as solved.

Thanks @petro and @123 for all the help!

2 Likes

Hey @Schneider. Awesome thread! I’m trying to do the same thing with my Google Homes by only sending HA notifications to media players that aren’t in state media_content_type: music (to avoid walking with a limp sometime real soon :flushed:).

However, I seem to be having issues with my ‘all google home groups’. I’ve set up groups both in the Google Home app and also in HA using the groups.yaml. All group entities are visible but when I play a song on one of my Google Homes, the device itself says ‘playing’ but the group status is off. Am I missing something?

You can see from the image below that the media player media_player.kitchen_home is playing but that the group above that it’s in is not and I don’t think your script will work without that functioning?

The group above is one created inside HA, the one below is the Google Home app.

Cracked it. Was going about it the wrong way. Final simplified code below. Edit defined media players and media player group (from Google Home App) which in my case is the media_player.upstairs_homes.

ghm_tts:
  sequence:
    - service: media_player.volume_set
      data_template:
        entity_id: >-
            {%- set players = ['media_player.kitchen_home', 'media_player.lounge_home'] %}
            {{ states.media_player | selectattr('state','!=','playing') | selectattr('entity_id', 'in', players) | map(attribute='entity_id') | join(', ') }}
        volume_level: '{{volume}}'
    - service: tts.google_say
      data_template:
        entity_id: >-
              {%- if states.media_player.kitchen_home.state != 'playing' and
              states.media_player.lounge_home.state != 'playing' %}
              media_player.upstairs_homes
              {%- else %}
              {%- set players = ['media_player.kitchen_home', 'media_player.lounge_home'] %}
              {{ states.media_player | selectattr('state','!=','playing') | selectattr('entity_id', 'in', players) | map(attribute='entity_id') | join(', ') }}
              {%- endif %}
        message: '{{tts}}'

## Tests ##
ghm_tts_test:
  sequence:
    - service: script.ghm_tts
      data_template:
        tts: "This is a notification that should not of played on speakers that are playing music"
        volume: 0.5
1 Like

Hello sir!

I am really sorry but did not see your previous message.

I am glad you’ve cracked it and made it even better, congratulations!

Have a great day!

Here’s how to create a script that doesn’t require modification should you ever add more media_players.

Create a group containing the two media_players on the main floor:

group:
  players_main:
    name: Media Players Main Floor
    entities:
      - media_player.kitchen_home
      - media_player.lounge_home

Now you can use the group in the script and refer to its members using the expand function.

ghm_tts:
  sequence:
    - service: media_player.volume_set
      data_template:
        entity_id: >-
            {{ expand('group.players_main') | selectattr('state','!=','playing') | map(attribute='entity_id') | join(', ') }}
        volume_level: '{{volume}}'

    - service: tts.google_say
      data_template:
        entity_id: >-
              {% set players = expand('group.players_main') | list %}
              {% set total = players | count %}
              {% set not_playing = players | selectattr('state','!=','playing') | list %}
              {% if not_playing | count == total %}
                media_player.upstairs_homes
              {% else %}
                {{ not_playing | map(attribute='entity_id') | join(', ') }}
        message: '{{tts}}'

Optional:

If you plan to add media_players upstairs then make a group to represent them (group.players_upstairs) and modify the template as follows:

    - service: tts.google_say
      data_template:
        entity_id: >-
              {% set players = expand('group.players_main') | list %}
              {% set total = players | count %}
              {% set not_playing = players | selectattr('state','!=','playing') | list %}
              {% if not_playing | count == total %}
                {% set floor = 'group.players_upstairs') %}
              {% else %}
                {% set floor = 'group.players_main') %}
              {% endif %}
              {{ expand(floor) | selectattr('state','!=','playing') | map(attribute='entity_id') | join(', ') }}
        message: '{{tts}}'

EDIT
Corrected typos. Replaced 'state','!=','players' with 'state','!=','playing'

little typo, should be ‘playing’ for select attr

Thanks! I was testing the templates using existing lights and then modified, all too quickly, for use with media_players (of which I have none for testing purposes).