Howto invoke tts.google targeting multiple entities from python_script

I need to invoke TTS on more than one entity, but I have a configuration wish: top avoid too many tts scripts each setup per message and different locations, I was thinking about writing a python_script which takes from an automation the target entities as data as well as the message, which is finally a script.

this is the automation - currently with a fake trigger (I trigger manually from dev tools):

- alias: test
  trigger:
    - platform: state
      entity_id: input_select.presence_all_preset
      to: Lange weg
  action:
    - service: python_script.test
      data:
        entities: 
          - media_player.squeezebox_kueche:60
          - media_player.squeezebox_az:10
        message: script.play_goodmorningmessage2

The python script parses the information, whereby the entities are containing the entity name and the target volume for the announcement.

This is the problematic script which actually contains the message. Invocation of the script works fine as well as tts with ONE entity:

play_goodmorningmessage2:
  alias: "Nachricht: Guten Morgen 2"
  sequence:
    - service: tts.google_say
      data_template:
        message: "Test für Python"
        entity_id: "{{ media_players }}"

the variable “media_players” contains at the moment a list which is passed to the following in python:

# for clarification, this is added in this post only and not part of the script (though I used it for testing)
tts = "script.play_goodmorningmessage2"
mediaplayers = [ 'media_player.squeezebox_kueche', 'media_player.squeezebox_sz' ]
# the above variable contents have been checked and are derived from the python scrypt

def play_announcement(mediaplayers,tts,hass):
    for entity in mediaplayers:
        service_data = { 'media_players': entity }
        hass.services.call('script', tts.split('.')[1], service_data, False)
    return

play_announcement(mediaplayers,tts,hass)

My problems:

  • I cannot invoke a script twice in parallel :-(. Is there anyway to do this? The above fails with the second target entity to play the same message. ANd I dont want to wait until the first one finishes (set False to True in Hass.service…)
  • I can have a simple script to announce a TTS message at two entities only by
play_message:
  alias: "Nachricht"
  sequence:
    - service: tts.google_say
      entity_id: media_player.squeezebx_kueche, media_player.squeezebox_sz
      data:
        message: "Test für Python"

This works, but seems to be against YAML structure. using a YAML list like

entity_id:
  - entity1
  - entity2

doesnt work.

who has an idea how I can send the required information to the script to allow it to play on multiple entities? Or - how can I use a script like the above in parallel for multiple entities?

Intention:

  • definition of multiple messages
  • definition of target entities for a message to be played
  • simple configuration of messages (they are complex templates actually), so that I can change a message at one place only and I dont need to change multiple scripts - which I would need to address multiple entities.

I hope the problem is understandable. Thanks a lot!

André

I have no idea what you are trying to do with that python script but it shouldn’t be working the way you have it configured. I’d expect your test python script to look like this:

# entities needs to be a yaml list.
mediaplayers = data.get('entities')
# message needs to be a string
message = data.get('message')

if mediaplayers and message:
    for entity in mediaplayers:
        service_data = { 'entity_id': entity }
        hass.services.call('tts', 'google_say', service_data, False)

and your action would be

    - service: python_script.test
      data:
        entities:
          - media_player.squeezebox_kueche
          - media_player.squeezebox_az
        message: "Test für Python"

No script, just the python script.

I also don’t see how this is any different from using the normal service… You aren’t saving any configuration time by going this route.

  alias: "Nachricht: Guten Morgen 2"
  sequence:
    - service: tts.google_say
      data:
        message: "Test für Python"
        entity_id:
          - media_player.squeezebox_kueche
          - media_player.squeezebox_az

And lastly, if you want multiple messages, you’ll need to make a message queue. You can’t send mulitple message to any device that speaks messages because it can only speak so fast. You have to build a queue and pull from the queue when the device is ready for another message.

Hello Petro,

thanks for the reply. I will try to explain better:
I do have 7 Audio devices. In my automation I have several messages I would like to play:
3 different types of weather alerts in different languages, 2 types of home appliance alerts and 3 other messages.
Depending on the situation (guests etc), the messages shall be played either on one or multiple devices at the same time.
At the moment the configuration is done as such:

  • for each message and each scenario where I want to play the message, I have to create a separate script. Simple case: 1 message, to be played back in 3 scenarios (mediaplayer1, mediaplayer 1 and 3, mediaplayer 4).
    This works. I am now extending the amount of messages and woudld like to be able to configure the playback (which messages in which scenarios) on the UI. Works as well.

If I now want to change the message, I have to update several scripts. If I want to update the target media_player I have to do the same.

My idea was to split out the message play back from target device and make it configurable, so that in an automation, I simply set a “variable” or list to define the target media player and another one to define the message. with this setup, I have one place where the message is defined, but I can play it back whereever I want.

This I achieved with. a Paython script parsing my data field and invoking the according services in HASS.

Downside:
I somehow cannot make a playback of the same message to multiple media-players at the same time (not synchronized, but more or less same time) work. In the automation I can do this as mentioned above using the tts.google_say service and provide “entity: mediaplayer1, mediaplayer2” which results in simultaneous playback of the same message in two players.
I didnt find a way in the Python script to achieve the same. I cannot provide a list to the script with the message. I assume that any variable {{ VAR }} is interpreted as string, and as such, I cannot get it to work.
The issue is only to invoke TTS playback of one message simultaneously on multiple devices. As it didnt work with a list, I started the script for each entity (different devices!), but of logically it is not allowed as this is the same script and normally you dont expect changing target media_players.

I will share more details tomorrow…

Ok, here we go. I am using different script and target players. For now I handle those with a script like this:

play_goodmorningmessage:
  alias: "Nachricht: Guten Morgen"
  sequence:
    - service: shell_command.playlist_save_sb
      data_template:
        mac: '00:04:20:29:f9:1c' # kitchen
        volume: '55' 
    - service: shell_command.playlist_save_sb
      data_template:
        mac: '00:04:20:29:df:b3' # bathroom
        volume: '45' 
    - service: tts.google_say
      entity_id: media_player.squeezebox_kueche, media_player.squeezebox_bad
      data_template:
        message: >
          {% if now().strftime("%H")|int < 12 %}
            Guten Morgen.
          {% elif now().strftime("%H")|int < 18 %}
            Guten Tag.
          {% else %}
            Guten Abend.
          {% endif %}
            Es sind {{states.sensor.dark_sky_temperature.state|round}} Grad in Berlin. Das Wetter:
          {% if 'Morgen' not in states('sensor.dark_sky_hourly_summary') %}
            {% if 'heute' not in states('sensor.dark_sky_hourly_summary') %}
              {% if 'Heute' not in states('sensor.dark_sky_hourly_summary') %}
                Heute
              {% endif %}
            {% endif %}
          {% endif %}
          {{states.sensor.dark_sky_hourly_summary.state|replace(".", "")}}. Die Tageshöchsttemperatur beträgt {{states.sensor.dark_sky_daytime_high_temperature_0d.state|round}} Grad.
          {% if states.sensor.dark_sky_precip_probability_1h.state|int > 35 and now().weekday() in (0,1,2,3,4) %}
            Für den Weg zur Arbeit mit dem Fahrrad besser eine Regenhose anziehen, die Regenwahrscheinlichkeit in 1 Stunde beträgt {{states.sensor.dark_sky_precip_probability_1h.state|round}} Prozent. 
          {% endif %}
    - service: shell_command.playlist_restore_sb
      data_template:
        mac: '00:04:20:29:f9:1c'
    - service: shell_command.playlist_restore_sb
      data_template:
        mac: '00:04:20:29:df:b3'

As you can see the message got some templating in it. Now I have the use case that based on situations (guests), the message should change by content slightly and language and the target media players should be different.
I can handle this in automation by splitting the above in:

  • 1 script which handles the pre and post work (saving and restoring state in the media_players - whihc the bash scripts do)
  • 1 or more scripts which contain the TTS command with different messages and I can hand the target script/message over as a variable.

Downside:
With automation and target media player templating, I cannot handover lists as in the above example to playback the same message simultaneously (not synchronously) in more than one media player. Additionally I dont have a global available mapper which associates entity with MAC address (if not available, I need to create scripts for each player and more are to come).
I thought I can make this happen in Python… but I codlnt figure out, how I can invoke the playback of the message to multiple players like in the automation…
Additionally I used Python to provide one global lookup table to associate media_player entities with MAC addresses as I need both, but I dont want to put the lookup mapper in every script for maintenance reasons (more players to come).

regards,

André

Python script below:

##########################################################################################
# Script to handle different announcements based on data provided
# ##########################################################################################
# set target squeezebox mediaplayer, volume and announcemtn script in the yaml file calling the script
#  automation example:
#    alias: announce something
#    sequence:
#      - service: python_script.make_announcement
#        data:
#          media_players:
#            - media_player.squeezebox_kueche:31    # entity_id:volume_for_announcement (0-100)
#            - media_player.squeezebox_az:40        # entity_id:volume_for_announcement
#          tts: script.announcement                 # entity_id
#
##########################################################################################

        
def save_mediaplayer_stat(mediaplayers,mac_address,volume,hass):
    for entity in mediaplayers:
        service_data = { 'mac': mac_address[entity], 'volume': volume[entity] }
        hass.services.call('shell_command', 'playlist_save_sb', service_data, False)
    return

def restore_mediaplayer_stat(mediaplayers,mac_address,hass):
    for entity in mediaplayers:
        service_data = { 'mac': mac_address[entity] }
        hass.services.call('shell_command', 'playlist_restore_sb', service_data, False)
    return

def play_announcement(mediaplayers,tts,hass):
    for entity in mediaplayers:
        service_data = { 'media_players': entity }
        hass.services.call('script', tts.split('.')[1], service_data, False)
    return

#######
# Main
#######

entities = data.get("entities", [])
mediaplayers = []
tts = data.get("message", "")
defaultvolume = 60   # 0-100
    
if tts in [None, ''] or len(entities) == 0:
    logger.error("Trying to announce something, but no announcement script or target mediaplayer provided!")
    quit() 

# internal variables
mac_address = {
    'media_player.squeezebox_kueche': '00:04:20:29:f9:1c',
    'media_player.squeezebox_wz': '00:04:20:23:89:f5',
    'media_player.squeezebox_bad': '00:04:20:29:df:b3',
    'media_player.squeezebox_gastbad': '00:04:20:2c:6f:73',
    'media_player.squeezebox_az': '00:04:20:2b:56:91',
    'media_player.squeezebox_kz': '00:04:20:2b:27:2e',
    'media_player.squeezebox_sz': '00:04:20:18:1f:7c',
}
volume = {
    'media_player.squeezebox_kueche': defaultvolume,
    'media_player.squeezebox_wz': defaultvolume,
    'media_player.squeezebox_bad': defaultvolume,
    'media_player.squeezebox_gastbad': defaultvolume,
    'media_player.squeezebox_az': defaultvolume,
    'media_player.squeezebox_kz': defaultvolume,
    'media_player.squeezebox_sz': defaultvolume,
}

# get target volume and mediaplayer entities
for item in entities:
    tmp_list = item.split(':')
    if len(tmp_list)>1 and tmp_list[1].isdigit() and 0<=int(tmp_list[1])<=100:
        volume[tmp_list[0]] = tmp_list[1]
    mediaplayers.append(tmp_list[0])

logger.warning("Making announcement to the following {} mediaplayer(s):".format(len(mediaplayers)))
for entity in mediaplayers:
    logger.warning("  - {}".format(entity) + " , volume: {}".format(volume[entity]))
logger.warning("Preparing to announce {}".format(tts))


save_mediaplayer_stat(mediaplayers,mac_address,volume,hass)
play_announcement(mediaplayers,tts,hass)
restore_mediaplayer_stat(mediaplayers,mac_address,hass)

I found my solution… I cannot invoke tts.google_say with multiple entities from python as it seems, but I can simulate the same behavior as in the first automation in which I can playback simultaneously by changing above play_announcment subfunction with chaging the boolean flag for waiting the service call to be executed to “True”. Thus the announcment are starting not synchronously, but simulatenously (nearly).

def play_announcement(mediaplayers,tts,hass):
    for entity in mediaplayers:
        service_data = { 'media_players': entity }
        hass.services.call('script', tts.split('.')[1], service_data, True)
    return

The above solution gives me the following advantages:

  • I dont need to write tts scripts for each target player or group of players - I simply specify the list in the automation call depending on my needs
  • I have a central place for my lookup table regarding media player entity -> MAC address

Thus the question can be closed as I found a workaround.

Hi Petro,

I solved my problem. The issue with the message queue I wanted to tackle later. E.g. Dont announce washingmachine ready, when nobody is home, but after you enter and it is not too late or similar.
I am not a developer and miss some experience - how would you implement a message queue in HA?
In my case, I would then let automations add entries to the queue (message + target players + target volume). I would need to have a function which checks the queue - and when not empty and somebody home, start announcing.

I have no clue how I can achieve this - with a hint, I will dig into this and get it to work (hopefully). I havent found any message queue example for HA. The beauty is that you can change the TTS component centrally, you can handle conditions when to playback messages centrally etc…

Thanks, Andre

You won’t be able to do it with python scripts as they are extremely limited and the memory doesn’t persist through each script execution. You’ll need appdeamon to handle a queue. Also, pretty sure @gpbenton has this rolling queue already setup in appdeamon.

EDIT: I do not know if his automation handles multiple media players, and I assume you’d have to change it around to handle your mac address stuff.

My script just uses espeak’s queue, so I don’t think its appropriate here.