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:
- Install Google SDK
- Create a helper to save the music playing speakers/groups
- Create a helper to save the music playing speaker volumes
- Create a script to manage processing of the message and volumes
- 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