Google SDK Broadcast Notifications Uninterpreted Music and Volume Restore

Briefly, I’ll share the design of my TTS Notification Engine using Google Assistant SDK Broadcasts for announcements. This is a simplified version of this amazing script which has unfortunate frequently unreliable dependencies on Spoticast/Spotify.

Features:

  • Music interrupted by a TTS Notification resumes (using Google SDK)
  • Restores previous music volume after script

Limitations:

  • Google SDK broadcasts are limited to ~30 words
  • Announcements are preceded by a chime and “Incoming broadcast says…”
  • Volume is restored after an arbitrary time delay of 10 seconds… I may look at message character counts to calculate approximate TTS.mp3 duration for the delay.

Google SDK Troubleshooting:

  • Make sure the speakers aren’t in do not disturb mode
  • Make sure the Home Assistant server is in the same network as the speakers
  • Broadcast to specific rooms often doesn’t work for non-English languages.
  • Some have suggested IPv6 must be disabled

Overview:

  1. Install Google SDK
  2. Create a helper to save the music playing speakers/groups
  3. Create a helper to save the music playing speaker volumes
  4. Create a script to manage processing of the message and volumes
  5. Call the script with the data in applicable automations

Components:

Save Music Volume: Template Sensor Helper. Rank speaker groups by largest to smallest, such that groups are prioritized over individual speakers.

{% set volume_list = [
{'entity':"media_player.everywhere", 'volume': state_attr('media_player.everywhere','volume_level'), 'state': is_state('media_player.everywhere','playing')},
{'entity':"media_player.main_living_area", 'volume': state_attr('media_player.main_living_area','volume_level'), 'state': is_state('media_player.main_living','playing')},
{'entity':"media_player.alerts", 'volume': state_attr('media_player.alerts','volume_level'), 'state': is_state('media_player.alerts','playing')},
{'entity':"media_player.downstairs", 'volume': state_attr('media_player.downstairs','volume_level'), 'state': is_state('media_player.downstairs','playing')},
{'entity':"media_player.upstairs", 'volume': state_attr('media_player.upstairs','volume_level'), 'state': is_state('media_player.upstairs','playing')},
{'entity':"media_player.living_room_speaker", 'volume': state_attr('media_player.living_room_speaker','volume_level'), 'state': is_state('media_player.living_room_speaker','playing')},
{'entity':"media_player.kitchen_display", 'volume': state_attr('media_player.kitchen_display','volume_level'), 'state': is_state('media_player.kitchen_display','playing')},
{'entity':"media_player.dining_room_speaker", 'volume': state_attr('media_player.dining_room_speaker','volume_level'), 'state': is_state('media_player.dining_room_speaker','playing')},
{'entity':"media_player.bathroom_speaker", 'volume': state_attr('media_player.bathroom_speaker','volume_level'), 'state': is_state('media_player.bathroom_speaker','playing')},
{'entity':"media_player.master_bedroom_speaker", 'volume': state_attr('media_player.master_bedroom_speaker','volume_level'), 'state': is_state('media_player.master_bedroom_speaker','playing')},
{'entity':"media_player.guest_bedroom_display", 'volume': state_attr('media_player.guest_bedroom_display','volume_level'), 'state': is_state('media_player.guest_bedroom_display','playing')},
{'entity':"media_player.basement_speaker", 'volume': state_attr('media_player.basement_speaker','volume_level'), 'state': is_state('media_player.basement_speaker','playing')},
{'entity':"media_player.living_room_speaker", 'volume': states('input_number.music_volume'), 'state': true},
]%}
{% set filtered_volume = volume_list|rejectattr('state','eq', false)| map(attribute='volume')|list %}
{{filtered_volume[0]|round(2)}}

Save Music Playing Speakers: Template Sensor Helper. Rank speaker groups by largest to smallest, such that groups are prioritized over individual speakers.

{% set speaker_list = [
{'entity':"media_player.everywhere", 'volume': state_attr('media_player.everywhere','volume_level'), 'state': is_state('media_player.everywhere','playing')},
{'entity':"media_player.main_living_area", 'volume': state_attr('media_player.main_living_area','volume_level'), 'state': is_state('media_player.main_living','playing')},
{'entity':"media_player.alerts", 'volume': state_attr('media_player.alerts','volume_level'), 'state': is_state('media_player.alerts','playing')},
{'entity':"media_player.downstairs", 'volume': state_attr('media_player.downstairs','volume_level'), 'state': is_state('media_player.downstairs','playing')},
{'entity':"media_player.upstairs", 'volume': state_attr('media_player.upstairs','volume_level'), 'state': is_state('media_player.upstairs','playing')},
{'entity':"media_player.living_room_speaker", 'volume': state_attr('media_player.living_room_speaker','volume_level'), 'state': is_state('media_player.living_room_speaker','playing')},
{'entity':"media_player.kitchen_display", 'volume': state_attr('media_player.kitchen_display','volume_level'), 'state': is_state('media_player.kitchen_display','playing')},
{'entity':"media_player.dining_room_speaker", 'volume': state_attr('media_player.dining_room_speaker','volume_level'), 'state': is_state('media_player.dining_room_speaker','playing')},
{'entity':"media_player.bathroom_speaker", 'volume': state_attr('media_player.bathroom_speaker','volume_level'), 'state': is_state('media_player.bathroom_speaker','playing')},
{'entity':"media_player.master_bedroom_speaker", 'volume': state_attr('media_player.master_bedroom_speaker','volume_level'), 'state': is_state('media_player.master_bedroom_speaker','playing')},
{'entity':"media_player.guest_bedroom_display", 'volume': state_attr('media_player.guest_bedroom_display','volume_level'), 'state': is_state('media_player.guest_bedroom_display','playing')},
{'entity':"media_player.basement_speaker", 'volume': state_attr('media_player.basement_speaker','volume_level'), 'state': is_state('media_player.basement_speaker','playing')},
{'entity':"media_player.living_room_speaker", 'volume': states('input_number.music_volume'), 'state': true},
]%}
{% set filtered_playing = speaker_list|rejectattr('state','eq', false)| map(attribute='entity')|list %}
{{filtered_playing[0]}}

TTS Notification Engine Script:

alias: TTS Notification Engine
sequence:
  - if:
      - condition: state
        entity_id: input_boolean.pause_alerts
        state: "on"
    then:
      - wait_for_trigger:
          - entity_id:
              - input_boolean.pause_alerts
            to: "off"
            from: "on"
            trigger: state
        timeout:
          hours: 1
          minutes: 0
          seconds: 0
          milliseconds: 0
        continue_on_timeout: true
    enabled: true
  - variables:
      message: "{{message}}"
      player: >-
        {{player|default(states('sensor.alerts_speaker_localization'))|replace('UserA',states('sensor.alerts_speaker_localization_UserA'))|replace('UserB',states('sensor.alerts_speaker_localization_UserB'))}}
      volume: "{{volume|default(states('input_number.notification_volume'))}}"
  - alias: Volume override
    if:
      - condition: or
        conditions:
          - condition: state
            entity_id: input_boolean.quiet_mode
            state: "on"
          - condition: state
            entity_id: binary_sensor.sleep_anyone
            state: "on"
    then:
      - variables:
          volume: "{{states('input_number.quiet_volume')}}"
  - data:
      agent_id: llm_agent_id
      text: "{{message}}"
    response_variable: llm_message
    action: conversation.process
  - variables:
      llm_message_1: >-
        {{llm_message.response.speech.plain.speech | trim | replace('"','') |
        replace('**Jarvis:**','') | replace("*Jarvis's voice*","") |
        replace("*Jarvis's sarcastic voice*","") | replace("\r"," ") |
        replace("\n"," ") | replace("Jarvis voice","") | replace("Jarvis voice
        with sarcasm","") | replace("[Jarvis' voice]","") |
        replace("Jarvis:","") | replace("Jarvis:","") | replace("JARVIS
        (V.O.)","") }}
      llm_message_2: >-
        {{ iif ('*UserA:*' in llm_message_1, llm_message_1.split('*UserA:*')[0],
        llm_message_1)}}
      llm_message_3: >-
        {{ iif ('**UserA:**' in llm_message_2,
        llm_message_2.split('**UserA:**')[0], llm_message_2)}}
      llm_message_4: >-
        {{ iif ("[UserA's voice]" in llm_message_3,
        llm_message_3.split("[UserA's voice]")[0], llm_message_3)}}
      llm_message_5: >-
        {{ iif ("UserA:" in llm_message_4, llm_message_4.split("UserA:")[0],
        llm_message_4)}}
      llm_message_6: >-
        {{ iif ("UserA:" in llm_message_5, llm_message_5.split("UserA:")[0],
        llm_message_5)}}
      llm_message_7: >-
        {{ iif ('*UserB:*' in llm_message_6,
        llm_message_6.split('*UserB:*')[0], llm_message_6)}}
      llm_message_8: >-
        {{ iif ('**UserB:**' in llm_message_7,
        llm_message_7.split('**UserB:**')[0], llm_message_7)}}
      llm_message_9: >-
        {{ iif ("[UserB's voice]" in llm_message_8,
        llm_message_8.split("[UserB's voice]")[0], llm_message_8)}}
      llm_message_10: >-
        {{ iif ("UserB:" in llm_message_9, llm_message_9.split("UserB:")[0],
        llm_message_9)}}
      llm_message_11: >-
        {{ iif ("UserB:" in llm_message_10,
        llm_message_10.split("UserB:")[0], llm_message_10)}}
      llm_message_final: >-
        {{llm_message_11 | replace('*','') | replace ("\n","") | replace(' 
        ','') | regex_replace(find="[^-\w\d\s!?,.:;']", replace='',
        ignorecase=False)}}
      tts_message: >-
        {{iif("SAFETY safety_ratings" in llm_message_final, message,
        llm_message_final)}}
  - event: set_variable
    event_data:
      key: message
      value: "{{tts_message}}"
  - alias: Play on speakers
    if:
      - condition: or
        conditions:
          - condition: state
            entity_id: media_player.living_room_speaker
            state: playing
          - condition: state
            entity_id: media_player.dining_room_speaker
            state: playing
          - condition: state
            entity_id: media_player.kitchen_display
            state: playing
          - condition: state
            entity_id: media_player.bathroom_speaker
            state: playing
          - condition: state
            entity_id: media_player.master_bedroom_speaker
            state: playing
          - condition: state
            entity_id: media_player.bathroom_speaker
            state: playing
        enabled: true
    then:
      - if:
          - condition: or
            conditions:
              - condition: state
                entity_id: binary_sensor.sleep_anyone
                state: "on"
              - condition: template
                value_template: "{{tts_message|wordcount>30}}"
        then:
          - action: automation.turn_off
            metadata: {}
            data:
              stop_actions: true
            target:
              entity_id: automation.volume_playing_speakers_sync
          - variables:
              resume_speaker: "{{states('sensor.media_speakers_playing')}}"
              resume_volume: "{{states('sensor.media_speakers_playing_volume_level')}}"
          - target:
              entity_id: "{{player}}"
            data:
              volume_level: "{{volume}}"
            action: media_player.volume_set
          - target:
              entity_id: "{{player}}"
            data:
              is_volume_muted: false
            action: media_player.volume_mute
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: 500
          - data:
              action:
                - service: tts.google_cloud_say
                  data:
                    cache: false
                    entity_id: "{{player}}"
                    message: "{{tts_message}}"
                  extra:
                    volume: "{{volume}}"
              target:
                entity_id: "{{resume_speaker}}"
            enabled: true
            action: script.google_home_resume
          - action: media_player.volume_set
            metadata: {}
            data:
              volume_level: "{{resume_volume}}"
            target:
              entity_id: "{{resume_speaker}}"
          - action: automation.turn_on
            metadata: {}
            data: {}
            target:
              entity_id: automation.volume_playing_speakers_sync
        else:
          - action: automation.turn_off
            metadata: {}
            data:
              stop_actions: true
            target:
              entity_id: automation.volume_playing_speakers_sync
          - variables:
              resume_speaker: "{{states('sensor.media_speakers_playing')}}"
              resume_volume: "{{states('sensor.media_speakers_playing_volume_level')}}"
              sdk_target: >-
                {{player|replace('media_player.','')|replace('_','
                ')|replace('guest_display','Guest
                Bedroom')|replace('kitchen_display','Kitchen')|replace('speaker','')}}
          - action: media_player.volume_mute
            metadata: {}
            data:
              is_volume_muted: true
            target:
              entity_id:
                - media_player.living_room_speaker
                - media_player.dining_room_speaker
                - media_player.kitchen_display
                - media_player.bathroom_speaker
                - media_player.guest_display
                - media_player.master_bedroom_speaker
          - action: media_player.volume_mute
            metadata: {}
            data:
              is_volume_muted: false
            target:
              entity_id: "{{player}}"
          - action: media_player.volume_set
            metadata: {}
            data:
              volume_level: "{{volume}}"
            target:
              entity_id: "{{player}}"
          - action: notify.google_assistant_sdk
            metadata: {}
            data:
              message: "{{tts_message}}"
              target: "{{sdk_target}}"
          - delay:
              hours: 0
              minutes: 0
              seconds: 10
              milliseconds: 0
          - action: media_player.volume_set
            metadata: {}
            data:
              volume_level: "{{resume_volume}}"
            target:
              entity_id: "{{player}}"
          - action: media_player.volume_mute
            metadata: {}
            data:
              is_volume_muted: false
            target:
              entity_id: "{{resume_speaker}}"
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: 500
          - action: automation.turn_on
            metadata: {}
            data: {}
            target:
              entity_id: automation.volume_playing_speakers_sync
    else:
      - if:
          - condition: state
            entity_id: automation.00_google_home_automatic_resume
            state: "on"
        then:
          - action: automation.turn_off
            metadata: {}
            data:
              stop_actions: true
            target:
              entity_id: automation.volume_playing_speakers_sync
          - target:
              entity_id: automation.00_google_home_automatic_resume
            data:
              stop_actions: true
            action: automation.turn_off
          - variables:
              resume_speaker: "{{states('sensor.media_speakers_playing')}}"
              resume_volume: "{{states('sensor.media_speakers_playing_volume_level')}}"
          - target:
              entity_id: "{{player}}"
            data:
              volume_level: "{{volume}}"
            action: media_player.volume_set
          - target:
              entity_id: "{{player}}"
            data:
              is_volume_muted: false
            action: media_player.volume_mute
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: 500
          - data:
              cache: false
              entity_id: "{{player}}"
              message: "{{tts_message}}"
            enabled: true
            action: tts.google_cloud_say
          - delay:
              hours: 0
              minutes: 0
              seconds: 30
              milliseconds: 0
          - action: media_player.volume_set
            metadata: {}
            data:
              volume_level: "{{resume_volume}}"
            target:
              entity_id: "{{player}}"
          - target:
              entity_id: automation.00_google_home_automatic_resume
            data: {}
            action: automation.turn_on
          - action: automation.turn_on
            metadata: {}
            data: {}
            target:
              entity_id: automation.volume_playing_speakers_sync
        else:
          - action: automation.turn_off
            metadata: {}
            data:
              stop_actions: true
            target:
              entity_id: automation.volume_playing_speakers_sync
          - variables:
              resume_speaker: "{{states('sensor.media_speakers_playing')}}"
              resume_volume: "{{states('sensor.media_speakers_playing_volume_level')}}"
          - target:
              entity_id: "{{player}}"
            data:
              volume_level: "{{volume}}"
            enabled: true
            action: media_player.volume_set
          - target:
              entity_id: "{{player}}"
            data:
              is_volume_muted: false
            action: media_player.volume_mute
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: 500
          - data:
              cache: false
              entity_id: "{{player}}"
              message: "{{tts_message}}"
            enabled: true
            action: tts.google_cloud_say
          - delay:
              hours: 0
              minutes: 0
              seconds: 30
              milliseconds: 0
          - action: media_player.volume_set
            metadata: {}
            data:
              volume_level: "{{resume_volume}}"
            target:
              entity_id: "{{player}}"
          - action: automation.turn_on
            metadata: {}
            data: {}
            target:
              entity_id: automation.volume_playing_speakers_sync
mode: queued

Minor Explanations:

  • Variables “message,” player," and “volume” are passed from the automation into the engine and have defaults and overrides specified in the engine
  • Pause Alerts allows alerts to be paused during a meeting/phone call
  • Player localizes the notification to the room of UserA and UserB or both per intent
  • LLM message modifications may be less important for more powerful models, but earlier models produced undesirable output requiring filtering for output dialogue
  • Playing the notification is handled such that (1) if speakers are playing music/podcasts and the message is <30 words, Google SDK handles the TTS request, (2) if speakers are playing and the message is >30 words, TheFes’ resume script handles the request, sometimes failing to resume the music, (3) if speakers are not playing then Google Cloud Say handles the TTS request.
  • My Volume Change Sync Automation is disabled during notifications

Good luck with your TTS Notification Engine

1 Like