Script for Sonos Speakers to do Text-to-Speech and Handle Typical Oddities

This script blueprint uses a Text-to-Speech service to play messages on Sonos speakers. The script
handles oddities to ensure a proper message playing experience. Examples include:

  • Saving the state of the Sonos and restoring when done (so music will stop and continue)
  • Handling speakers in groups (for both old and newer versions of HA)
  • Pausing music so volume adjustments don’t impact current music
  • Applies volume to all grouped speakers
  • Independent of volume, unmutes all grouped speakers
  • Disabling repeat so that the announcement doesn’t repeat
  • Handling delays to ensure proper handling of volume setting and playback cut-offs

The following field parameters can be given when the script is called:

  • [required] Sonos speaker
  • [required] Text of the message to say
  • [optional] Volume for message playback. This only impacts the indicated speaker, not the group, and reverts when done.
  • [optional] Minimum number of seconds that the system will wait after state changes in Sonos

The following blueprint inputs can be given when creating the script:

  • [optional] Text-to-Speech Service engine to use to say the specified message. Default is google_translate_say.
  • [optional] Language to say messages in. Default is en.

This script is particularly convenient when:

  • You want to announce something on a Sonos speaker but want the speakers to continue what they were doing when playback completes (e.g. opening a dishwasher and announcing the dishes are clean on a kitchen speaker)

Additional Notes

  • I recommend setting the mode to parallel when using the same script for multiple automations
     

Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.

# ***************************************************************************
# *  Copyright 2022-2023 Joseph Molnar
# *
# *  Licensed under the Apache License, Version 2.0 (the "License");
# *  you may not use this file except in compliance with the License.
# *  You may obtain a copy of the License at
# *
# *      http://www.apache.org/licenses/LICENSE-2.0
# *
# *  Unless required by applicable law or agreed to in writing, software
# *  distributed under the License is distributed on an "AS IS" BASIS,
# *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# *  See the License for the specific language governing permissions and
# *  limitations under the License.
# ***************************************************************************

# Announces the provided message on the specified speaker.

blueprint:
  name: "Text-to-Speech on Sonos"
  description:
    This blueprint is used to add a script that will say messages on Sonos speakers. The script
    handles oddities to ensure a proper experience including saving/restore state, handling
    speaker groups, unmuting, pausing music, disabling repeat, adding delays, etc.

    I recommend setting the mode to parallel if you will use this script on more than one speaker.
  domain: script
  input:
    tts_service_name:
      name: Text-To-Speech Service Name
      description:
        The text-to-speech service to use when saying the message. This must match your Home
        Assistant configuration.
      default: "google_translate_say"
      selector:
        text:
    tts_language:
      name: Language
      description: The language to use with the text-to-speech service.
      default: "en"
      selector:
        text:

fields:
  entity_id:
    description: The entity id of the Sonos speaker that will play the message.
    name: Entity
    required: true
    selector:
      entity:
        domain: media_player
        integration: sonos
  message:
    description: The text that will be played.
    name: Message
    required: true
    selector:
      text:
  volume_level:
    name: "Volume Level"
    description:
      Float for volume level. Range 0..1. If value isn't given, volume isn't changed.
      The volume will revert to the previous level after it plays the message.
    required: false
    selector:
      number:
        min: 0
        max: 1
        step: 0.01
        mode: slider
  min_wait:
    name: "Minimum Wait"
    description:
      The minimum number of seconds that the system will wait for state changes from
      Sonos. Frequently the Sonos integration reports state changes too early, misses
      some state quick enough which can result in odd volume changes, cut-off messages
      and even, when the message is very short, long delays before continuing to play
      previously running media. Setting this value will help. Defaults to 0 if not set.
    required: false
    selector:
      number:
        min: 0
        max: 60
        step: 0.25
        unit_of_measurement: seconds
        mode: slider

  max_wait:
    name: "Maximum Wait"
    description: THIS IS DEPRECATED AND WILL BE REMOVED IN FUTURE VERSIONS
    required: false
    selector:
      number:
        min: 1
        max: 60
        step: 0.25
        unit_of_measurement: seconds
        mode: slider

variables:
  entity_group: >-
    {# some operations we will be doing against the group, so need the group #}
    {%- set group_members = state_attr( entity_id, "group_members" ) -%}
    {%- if group_members == None -%}
      {# we maybe on an older version of HA, so look for a different group name#}
      {%- set group_members = state_attr( entity_id, "sonos_group" ) -%}
      {%- if group_members == None -%}
        {{ entity_id }}
      {%- else -%}
        {{ group_members | join(', ') }}
      {%- endif -%}
    {%- else -%}
      {{ group_members | join(', ') }}
    {%- endif -%}
  entity_group_leader: >-
    {# we see if in a group since the repeat is typically controlled by it #}
    {# we use this for doing all the work since it is the primary speaker #}
    {# and everything will be shared across speakers anyhow #}
    {%- set group_members = state_attr( entity_id, "group_members" ) -%}
    {%- if group_members == None -%}
      {# we maybe on an older version of HA, so look for a different group name#}
      {%- set group_members = state_attr( entity_id, "sonos_group" ) -%}
      {%- if group_members == None -%}
        {{ entity_id }}
      {%- else -%}
        {{ group_members[0] }}
      {%- endif -%}
    {%- else -%}
      {# the first seems to be the control, at least on Sonos #}
      {{ group_members[0] }}
    {%- endif -%}
  entity_repeat_state: >-
    {# we grab the repeat state so that if in repeat mode we turn off #}
    {# and also sanity check that we got a value otherwise default to off #}
    {%- set repeat = state_attr( entity_group_leader, "repeat" ) -%}
    {%- if repeat == None -%}
      off
    {%- else -%}
      {{ repeat }}
    {%- endif -%}

  # oddly you can't get blueprint inputs directly into jina so . . .
  # 1) putting service into a variable to verify values and provide default
  tts_hack: !input tts_service_name
  tts_engine: >-
    {%- if tts_hack is undefined or tts_hack== None or tts_hack == "" -%}
      tts.google_translate_say
    {%- else -%}
      tts.{{ tts_hack }}
    {%- endif -%}
  # 2) putting language into a variable to verify values and provide default
  lang_hack: !input tts_language
  tts_language: >-
    {%- if lang_hack is undefined or lang_hack== None or lang_hack == "" -%}
      "en"
    {%- else -%}
      {{ lang_hack }}
    {%- endif -%}
  # the state delay is used for forcing a wait after key state changes
  # since the Sonos integration seems to report state too early which can cause
  # audio being cut-off or volume changes being heard that shouldn't be.
  state_delay: >-
    {%- if min_wait is undefined or min_wait == None or not (min_wait is number) or min_wait < 0 -%}
      {# bad or missing data means we just use a default of 0 #}
      00:00:00
    {%- else -%}
      {{ "00:00:" + "{:02d}".format(min_wait | int ) + "." +  "{:03d}".format( ( ( min_wait - ( min_wait | int ) ) * 1000 ) | int ) }}
    {%- endif -%}

sequence:
  # save current state so we can restore to whatever was happening previously
  - service: sonos.snapshot
    data:
      entity_id: "{{ entity_group_leader }}"
      with_group: true

  # if something is playing, we pause...this is nice if you are changing the
  # volume since you won't hear the volume adjust on what is currently playing
  - choose:
      - conditions:
          - condition: template
            value_template: >
              {{ is_state(entity_group_leader, 'playing') }}
        sequence:
          # so we pause (using the leader, since that will cover all)
          - service: media_player.media_pause
            data:
              entity_id: "{{ entity_group_leader }}"
          # do a quick to make sure it isn't playing
          - wait_template: "{{ states( entity_group_leader ) != 'playing' }}"
            timeout:
              seconds: 2
          # we then put in a slight delay to ensure the pause
          - delay: >-
              {{ state_delay }}
    default: []

  # we check to see if the player is in repeat state and turn off otherwise
  # the alarm announcement will repeat forever
  - choose:
      - conditions: >
          {{ entity_repeat_state != "off" }}
        sequence:
          - service: media_player.repeat_set
            data:
              repeat: "off"
              entity_id: "{{ entity_group_leader }}"
          - wait_template: "{{ state_attr( entity_group_leader, 'repeat' ) == 'off' }}"
            timeout:
              seconds: 4
    default: []

  # we set the volume if it is defined, but do so far all speakers in the group
  - choose:
      - conditions: >
          {{ volume_level is defined and volume_level != None and volume_level is number }}
        sequence:
          # now we can set the set the volume
          - service: media_player.volume_set
            data:
              volume_level: "{{ volume_level }}"
              entity_id: >
                {{ entity_group }}

  # force to unmute, just in case, since mute IS different than volume
  - service: media_player.volume_mute
    data:
      entity_id: >
        {{ entity_group }}
      is_volume_muted: false

  # FINALLY the actual call to say the message
  - service: "{{ tts_engine }}"
    data:
      entity_id: "{{ entity_group_leader }}"
      message: "{{ message }}"
      language: "{{ tts_language }}"

  # not sure why, but setting the repeat doesn't always properly take
  # I can see it changing in the state  when the TTY goes the repeat
  # turns back on ... calling to turn off again helps
  - service: media_player.repeat_set
    data:
      repeat: "off"
      entity_id: "{{ entity_group_leader }}"

  # first we wait for it to start to properly announce the time
  - wait_template: "{{ states( entity_group_leader ) == 'playing' }}"
    timeout:
      seconds: 2 # timeout so doesn't sit forever

  # we put a slight delay in here to ensure that the grabbing of the media
  # duration below is likely to succeed, which tends to be crucial for short
  # messages, otherwise it seems to take the full media length from what was
  # previously playing
  - delay: >-
      {{ state_delay }}

  # then we wait for it to finish announcing before we continue
  - wait_template: "{{ states( entity_group_leader ) != 'playing' }}"
    timeout: >-
      {# we grab the duration to try to set a wait that is roughly the right amount of time #}
      {# this is returned in seconds, so not extact accurate #}
      {% set duration = state_attr(entity_group_leader, 'media_duration') %} 
      {% if duration == None or duration <= 1 %} 
        {# this should never happen, though sounds like there can be delays in response #}
        {# to get the state, so we put a mininum of one second ... the waiting for the state #}
        {# below should cover BUT if it doesn't than state_delay can make sure we are good #}
        {{ "00:00:01" }}
      {% else %} 
        {# adding a second, just to help with potential cut-off #}
        {% set duration = duration + 1 %} 
        {% set seconds = duration % 60 %} 
        {% set minutes = (duration / 60)|int % 60 %} 
        {% set hours = (duration / 3600)|int %} 
        {{ "{:02d}".format(hours) + ":" + "{:02d}".format(minutes) + ":" + "{:02d}".format(seconds) }}
      {% endif %}

  # we then put in a slight delay to ensure the playing finished
  - delay: >-
      {{ state_delay }}

  # and now we restore where we were which should cover repeat, what's playing, etc.
  # NOTE: so far this works when driven by Sonos or HA, but when driven from Alexa
  #       it doesn't seem to work as well
  - service: sonos.restore
    data:
      entity_id: "{{ entity_group_leader }}"
      with_group: true

mode: parallel
max_exceeded: silent
icon: mdi:account-voice

Revisions

  • 2023-01-14: volume_level, if given, is applied to all grouped speakers, simplified wait handling (should never wait unexpectantly long anymore and deprecated max_wait), and forcibly unmutes speakers based on feedback from @0_0
  • 2022-10-28: Better volume value checking and added delay afer playing the message to help short message playback
  • 2022-10-15: Added min_wait to help with HA reporting Sonos state changes too early, and changed max_wait and delay handling based on feedback from @krazykaz83
  • 2022-05-25: Initial release

Available Blueprints

16 Likes

My Sonos-related scripts continue … I commonly use my Sonos speakers to talk to me (e.g. tell me my dishes are clean, when alarms will go off, etc.). I created this script to make my life easier since I found a good number of oddities and desired behaviours that need handling when using text-to-speech (see above description for details).

In general, my intention is to provide high quality blueprints that ease automation significantly. If you have any feedback, please send it my way.

5 Likes

Really cool! Have been experimenting with TTS a bit, but this will ease my life! tnx

@mhoogenbosch , glad it will help! If you have any other considerations, let me know.

Hi @Talvish

Started using your script because the other one started having issues with playing the message over and over again.

This is what I used:

sonos_say:
  alias: Sonos TTS script
  sequence:
  - service: sonos.snapshot
    data_template:
      entity_id: '{{ sonos_entity }}'
  - service: sonos.unjoin
    data_template:
      entity_id: '{{ sonos_entity }}'
  - service: media_player.volume_set
    data_template:
      entity_id: '{{ sonos_entity }}'
      volume_level: '{{ volume|default(0.5) }}'
  - service: tts.google_say
    data_template:
      entity_id: '{{ sonos_entity }}'
      message: '{{ message }}'
  - delay: '{{ delay|default(''00:00:01'') }}'
  - wait_template: '{{ is_state(sonos_entity, ''playing'') }}'
    timeout: 00:00:03
  - wait_template: '{{ not is_state(sonos_entity, ''playing'') }}'
    timeout: 00:01:00
  - service: sonos.restore
    data_template:
      entity_id: '{{ sonos_entity }}'

Now your script works great in automations but not in the “kalkih/mini-media-player” while the other one did work (minus the repeating part ofc)

type: custom:mini-media-player
entity: media_player.woonkamer
icon: mdi:speaker-wireless
artwork: cover
tts:
  platform: sonos
  entity_id: media_player.woonkamer
  volume: 0.5
hide:
  power: true
speaker_group:
  platform: sonos
  show_group_count: true
  entities:
    - entity_id: media_player.woonkamer
      name: Woonkamer

I’m getting “Failed to call service script/sonos_say. UndefinedError: ‘entity_id’ is undefined
It seems the entity_id isn’t passed through. No idea if the error is in the mini player (tts info media player here: GitHub - kalkih/mini-media-player: Minimalistic media card for Home Assistant Lovelace UI) or in your code.

afbeelding

Hi there,

Not sure how helpful I’ll be since I don’t know the media player, but some questions and then an assumption-based recommendation at the end.

Question 1: Am I reading this right that you aren’t actually using the script I created but you made a variant which is below?

Question 2: And you put this in your scripts.yaml?

But this script isn’t declaring any fields so not sure how sonos_entity (nor volume or message, etc) would be defined. And if there is no definition then the call to any other service (e.g. sonos.snapshot) will fail since sonos_entity will be undefined (likely resulting in the error you are seeing).

Question 3: How does …

… make it to your sonos_say script, since it isn’t a true TTS implementation?

But on the script side, if I make some assumptions, it would be that you need to declare the fields in your script and if the tts reference is assuming the field name being passed in is entity_id, you should use a field name of entity_id instead of sonos_entity. For example in my script the field is this:

fields:
  entity_id:
    description: The entity id of the Sonos speaker that will play the message.
    name: Entity
    required: true
    selector:
      entity:
        domain: media_player
        integration: sonos

Again, big assumption on my behalf on the way things work, but my suspicion is your code should be more like the following (below does work). It has the fields, it also properly calls google_translate_say (there may be a google_say, but I don’t have it on my system):

sonos_saying:
  alias: Sonos TTS script
  fields:
    entity_id:
      description: The entity id of the Sonos speaker that will play the message.
      name: Entity
      required: true
      selector:
        entity:
          domain: media_player
          integration: sonos
    message:
      description: The entity id of the Sonos speaker that will play the message.
      name: Message
      selector:
        text:
    volume:
      name: Volume
      required: true
      selector:
        number:
          max: 1
          min: 0
          step: 0.01
          mode: slider
    delay:
      name: Delay
      selector:
        number:
          max: 0
          min: 10
  sequence:
    - service: sonos.snapshot
      data:
        entity_id: "{{ entity_id }}"
    - service: sonos.unjoin
      data:
        entity_id: "{{ entity_id }}"
    - service: media_player.volume_set
      data:
        entity_id: "{{ entity_id }}"
        volume_level: "{{ volume|default(0.5) }}"
    - service: tts.google_translate_say
      data:
        entity_id: "{{ entity_id }}"
        message: "{{ message }}"
    - delay: "{{ delay|default('00:00:01') }}"
    - wait_template: "{{ is_state(entity_id, 'playing') }}"
      timeout: 00:00:03
    - wait_template: "{{ not is_state(entity_id, 'playing') }}"
      timeout: 00:01:00
    - service: sonos.restore
      data:
        entity_id: "{{ entity_id }}"

Hope this helps!

One more comment. When making a script, the best way to know if it is working is to use this page on your local instance:

Service / Developer Tools

It allows you to test your scripts, change parameter values, etc. I find it indispensable.

Hi @Talvish

Thanks for taking the time to answer my question but sorry if I wasn’t clear enough.

The first script was something I used before your blueprint. With this script the kalkih mini-media-player lovelace was working fine.

About how does tts: make it in my script?
It’s an object of the mini media player card.

Like I said, your script is working perfectly with automatisations and I confirmed this with a developeroptions/service test that is working too.
afbeelding

The card is calling the script/sonos_say but seems confused about the entity_id but you gave me some pointers where to look at. I will try it out and let you know!
afbeelding
afbeelding

edit: I called google_translate_say → google_say
afbeelding

edit 2:
Still no idea why it’s asking for entity_id, my coding and code reading skills are to low sadly.
afbeelding
more code here: mini-media-player/tts.js at 10d6a1d69c657ad1234902eceb26d968ebf4e58d · kalkih/mini-media-player · GitHub

edit 3: Oh ok…
sonos²
² Does not support language & entity_id options.
So defining the entity_id in the card was useless, now to figure out how this code bellow works.
afbeelding

Thanks for the blueprint. I’m new at HA and still learning so hope you don’t mind a question. I dowloaded the blueprint and used it to make a script. As best as I can see the script really only asked me for the speaker to target and message to send. It works when I add it to my ‘washer/dryer’ is done automations but it stops the Sonos queue from continuing to play. Where does the configuration for that get set? That’s the functionality that really sounds great and unique. Thanks for steering me in the right direction.

Okay, I see … my JavaScript is a bit rusty, and unfortunately, I don’t have a way to test this, but I suspect for the case ‘sonos’ (which is presumably calling me and something you hand added), it should be more like the code below. I put comments on the lines I changed . …

      case 'sonos':
        this.hass.callService('script', 'sonos_say', {
          entity_id: opts.entity_id,                  // I don't have a field called `sonos_entity`
          volume_level: config.volume || 0.5,         // I don't have a field called `volume` (note: 0.5 will be loud if config.volume doesn't exist)
          message : message,                          // I think this should be okay, but not 100% sure 
          ...config.data,                             // doubt this line is needed since I only accept on more field, max_wait, and not sure if you have a way to set it
        });
        break;

Let me know if that works for you or what error you get.

Edit: . . . .

Now I fully understand the issue. I just realized that the github link you sent (mini-media-player/tts.js ) happens to call a script called sonos_say BUT it isn’t my script it is expecting, but another older script (aka the one you originally quoted to me). Who knew; I wrote something similar to someone else.

To make this work, you need to update the mini-player’s JavaScript for the sonos case, and replace with what I wrote above (I suspect it will work but again my JavaScript is rusty). I’ve never downloaded nor installed custom UI components, so not sure how they work BUT I suspect the JavaScript is in your HA instance and you can hand modify. If you get an error, let me know.

Hi billraff,

It should have happened automatically, there is no config. It relies on a Sonos integration mechanism that saves and restores the Sonos state. It has been tested on TuneIn, Spotify and Pandora.

Two questions:

  1. What music service are you using?
  2. Which version of HA you using?

Thanks for the information. My HA is 2022.6.4. I was testing playing a playlist on Apple Music. I can test with Pandora later today. Drinking coffee while the grandkids sleep this morning so don’t want to wake them.

Kids woke up so tested with Pandora. I started a station on Pandora and then ran the script. The announcement played but the music stopped. Here is the trace -

Choose: Default action executed

washer is done (script.1654728782683) started
(script.1654728782683) turned on
(script.1654728782683) turned off
(media_player.sewing2) turned playing
1 second later

(media_player.sewing2) turned paused

I’m running 2022.5.x still. I usually wait a bit to upgrade and there were some Sonos integration changes (and reverts apparently). I’ll upgrade shortly and see if my script behaves differently in 2022.6.x.

Update: I just upgraded to 2022.6.4 and I tested with Spotify and TuneIn, they worked. I had a friend test Pandora previously, so I’ll see if still works for him.

Works great for me on 2022.6.4 too. With Spotify and TuneIn and even normaal tv.

Thanks for checking Martijn. Definitely a curious one.

@billraff, I may be able to build a work around this weekend, but want to do some digging first. Any more information to help?

  1. The script/automation you wrote that calls my script, does it happen to do something interesting with the Sonos after, or perhaps force quit (stretching here, but figured I may as well ask :slight_smile: )?
  2. Is your Sonos an S1 or S2 system (not sure if that makes a difference, but mine are S2).
  3. What type of speaker? I might be able to test on the similar speaker. My testing was on a Beam, Ones and Fives (grouped and ungrouped), but I can test on an Arc, Roam, Move and Play:3s as well.

Quick update: I spoke to my friend who uses Pandora and they had no problems either. If you have a chance to answer the questions above I’ll do a bit more digging.

Would not be surprised to find my environment or usage is unique since others are not seeing the same behavior. No need to build anything special for me. The automation I wanted to use your script on is from a blueprint found here on this site as well.

Basically it sends a notification when our washer and dryer is finished. It knows this since I can monitor
electrical usage from a Sonoff outlet the dryer and washer each have.
But for testing your script this morning I simply used the developer tools. Here is the script:

All my speakers are S2 but they are also running on SonosNet (WM:0) to try and cut down on the amount of wifi traffic. So there is a Sonos Boost in the picture. I have a total of 12 Sonos products. The ‘cave1’ I used in the Developer tools test is actually a pair of Sonos Ones that have been joined. I tested using just a single speaker in the dining room and saw the same result.

(script.1654728782683) turned on

Call service tts.google_translate_say

(script.1654728782683) turned off
(media_player.dining_room) turned playing
2 seconds later

(media_player.dining_room) turned paused

Finished at June 12, 2022, 7:51:02 AM (runtime: 0.08 seconds)

Somehow it wants to pause after playing the announcement.

The Sonos integration was one of the first things I set up in HA. My whole Home Assistance adventure was because I bought a Sonos Port and hooked it up to my Yamaha AVR and wanted to turn on the AVR once Sonos started playing music and set the AVR to the proper input/output. Could have bought an Onkyo AVR that ‘works with Sonos’ to do that but HA was much cheaper. I think tomorrow I’ll remove the Sonos integration and re-add to freshen things. I’ll report back afterwards.

From the screenshot you sent it isn’t calling my script but is calling tts.google_translate_say directly. So just to confirm, when you said you tried using the dev tools, did you …

  1. Get some music playing
  2. Go here: Developer Tools – Home Assistant
  3. Fill-out the form (the script name is likely script.sonos_play and will show Script: Text-to-Speech on Sonos)
  4. Submit

Example: