How to notify using TTS — and much more

Tags: #<Tag:0x00007f7399a3b8d0> #<Tag:0x00007f7399a3b790> #<Tag:0x00007f7399a3b628>

The situation

In HA, we often use notify and notify groups to alert certain situations.

We have a lot of notify integrations but none for TTS.

In this How-To, you’ll learn how to

  • Use variables and inputs to set a default TTS media player and TTS language.
  • Create a long-lived token for REST API access.
  • Make a notify.tts_google service, as an example for simple TTS notify services.
  • Make a versatile notify.tts service for PicoTTS, using a script.
  • Use our new notify.tts service in an automation.
  • Use notify groups.
  • Use a TTS notifier in a HA alert.

Note: I’ve tested this on HA Core 0.112.2 and 0.112.3. Your mileage may vary.

TTS setup

I have set up my tts: section in configuration.yaml to use both PicoTTS and Google Translate TTS. Keep in mind that certain values like cache can be only defined once, in the first entry, and are also valid for the other entries.

# Text to speech
tts:
  - platform: picotts
    language: 'en-GB'
    base_url: https://has1.example.com
    cache: false
  - platform: google_translate
    base_url: https://has1.example.com
    language: 'en'

A default media player for TTS, and pre-recorded sound bits

I use the var custom component to hold the entity ID of my default media player for TTS messages and have it set up as follows:

# Custom variables component (in "custom_components/var/")
var:
  audio_alerts_media_player:
    friendly_name: "Default Media Player for Audio Alerts"
    initial_value: media_player.signalpi1

  audio_alerts_base_url:
    friendly_name: "Base URL for audio alert messages"
    initial_value: "https://has1.example.com/local/alerts"

  audio_alerts_base_path:
    friendly_name: "Base (internal) path for audio alerts"
    initial_value: "/home/homeassistant/.homeassistant/www/alerts"

As you can see, I also store a base URL and path for pre-recorded audio alerts and snippets, mainly for PicoTTS because it supports adding sound bits to a message. (The path will be different if you don’t use HA Core.)

My folder structure for these looks like this:

www/
  alerts/
    de/
      picotts-beep.wav
      picotts-alert09.wav
      something.mp3
      …
    en/
      picotts-beep.wav
      picotts-alert09.wav
      something.mp3
      …

You see the files have the same name, but are recorded in different languages. I make the same distinction by language for TTS commands.

If you do not wish to install the var component (or use another), just change the example code accordingly and insert a fixed value instead of using the variable. It’s easy.

Multi-lingual households: Select a TTS language

I’m in a multilingual household, so I wanted the TTS language to be switchable.

This is done by adding an input_select with the needed languages, and of course by writing your automations and scripts accordingly. Since some services want a two-letter code like en and others a code like en-GB, I opted to use the longer one for the input selections and simply use only the first two letters of the “long code” for those services requiring a two-letter code.

input_select:
  audio_language:
    name: 'Audio Language'
    # first 2 chars will be used as subfolder name for pre-recorded audio alerts
    # full name will be used as language input to PicoTTS, first two letters for Google TTS
    options:
      - 'en-GB'
      - 'de-DE'

If you do not need that, just replace it in the code with a hard-coded value.

Create a Long-lived REST API Token

We need a “long-lived access token” to easily access our own HA instance via the REST API. If you don’t already, have one, create it:

Click on your Profile button (bottom left in the HA menu, where your name is):

profile-button
Activate Advanced Mode:

advanced-mode

Scroll down to the bottom of your profile, to Long-Lived Access Tokens and click on Create Token:

long-lived-tokens

Give it the name REST:

long-lived-token-create-1

A popup with your new token appears:

long-lived-token-create-2

Important: Copy the token before you click OK and store it immediately in your secrets.yaml file:

# Long-lived (10y) token for REST API access (NOTE: Need to include "Bearer" here!)
ha_token_rest: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI2YWU1YTA2MmVmZjE0MjMxODZiMTE5NzdlYWM5MzU1MiIsImlhdCI6MTU5NDAwMzcxMCwiZXhwIjoxOTA5MzYzNzEwfQ.LANS95Y6TzTlUOMgwsJ73VRiyvMKDKaSMi__ZrxJLx8'

Important:

  • The token is a very long string, be sure to copy all of it! (Above token shown is a fake.)
  • Enclose the token in single apostrophes and add the text "Bearer " in front of it, as shown above.

You can now click OK in the HA UI. Your new token should now be shown under Long-Lived Access Tokens.

Note: This process only needs to be done once (well, every 10 years …). The token can now be used to authenticate ourselves against HA for all REST API calls.

Our first service: notify.tts_google

Setup

This is an example for making a simple notify TTS service, using HA’s REST API.
You should be able to easily adapt it to other TTS services.

In your configuration_yaml, under notify:, enter this:

# Notifications
notify:

  # Workaround TTS notifier, uses REST and Google Translate say
  # This can be used with the "alert" integration.
  # I tried to make it as compatible as possible with existing 'notify' integrations.
  - name: tts_google
    platform: rest
    resource: https://has1.example.com/api/services/tts/google_translate_say
    method: POST_JSON
    headers:
      Authorization: !secret ha_token_rest
      content-type: application/json
    data_template:
      # Notifications usually have a title and a message,
      # so prepend the title to the message and insert
      # a newline in between so the TTS engine makes a longer pause.
      message: "{{ title ~ '\n' ~ message }}"

      # Optional extras which can also be set in an alert's 'data' attribute.
      # For 'notify', these are presented in the service data attribute 'data'.
      # Since we have no script, we need to take care of missing values here.
      entity_id: "{{ data.entity_id | default(states('var.audio_alerts_media_player'),true) }}"
      language: "{{ (data.language | default(states('input_select.audio_language'), true))[0:2] }}"

Important:

  • Change has1.example.com in the resource: line to point to your HA server’s address!

  • If not using the var: custom component, change the entity_id: line to something like the following, putting in the entity_id of your desired media player:

    entity_id: "{{ data.entity_id | default('media_player.signalpi1',true) }}"
    
  • If not using input_select.audio_language, change the language: line to reflect your desired language:

    language: "{{ data.language | default('en', true) }}"
    
  • You must restart HA after adding/making changes to this notifier.

Try it out!

After setting everything up, it’s time to try out your new notify.tts_google notifier.

Enter Developer Tools → Services and try out the following:

Did it work? (If not, try to find out why, and repair it.)

Congratulations! Your first TTS notifier works!

A versatile notify.tts service for PicoTTS, using a script.

Now that we’ve learned a few things about REST and notifiers and made our first simple one, let’s make a more elaborate one using PicoTTS and a script.

Why?

  • PicoTTS can be used “cloud-free”, without Google or Amazon ever knowing what we speak.
  • Using a script allows for easy changes later on, without having to modify things in a zillion places in configuration.yaml ever again.
  • A script can do nice extras, like adding a short sound file at the beginning of your spoken messages.
  • A script is a good place to check variables, and provide good defaults for missing ones.
  • With a script, you could do things like turn off all audio alerts, based on some boolean input or sensor data.

Setup

First, let’s add another notifer under the notify: section in your configuration.yaml, much like the one for Google TTS:

  # Workaround TTS notifier, uses REST and the TTS script "Say" for audio alerts.
  # This can be used with the "alert" integration.
  # I tried to make it as compatible as possible with existing 'notify' integrations.
  - name: tts
    platform: rest
    resource: https://has1.example.com/api/services/script/say
    method: POST_JSON
    headers:
      Authorization: !secret ha_token_rest
      content-type: application/json
    data_template:
      # Notifications usually have a title and a message,
      # so prepend the title to the message and insert
      # a newline in between so the TTS engine makes a longer pause.
      message: "{{ title ~ '\n' ~ message }}"

      # Optional extras which can also be set in an alert's 'data' attribute.
      # For 'notify', these are presented in the service data attribute 'data'.
      # 'script.say' takes care of missing fields or such with a 'None' value.
      entity_id: "{{ data.entity_id }}"
      language: "{{ data.language }}"
      audiofile: "{{ data.audiofile | default('picotts-alert09.wav', False) }}"

As explained above, adapt the resource: and possibly audiofile: lines to your needs.

You might have noticed that the resource: line now wants to access the /api/services/script/say endpoint, instead of the /api/services/tts/google_translate_say endpoint we used for our Google TTS example above.

So we need a script that will handle the TTS notify call. Put it into your scripts.yaml file:

# Say
# Plays audio file given by "audiofile" variable to media player given by "entity_id"
# Then plays TTS message to same player
say:
  alias: 'Say'
  description: 'Speak a message via TTS'
  fields:
    entity_id:
      description: 'Entity Id of the media player to use'
      example: 'media_player.signalpi1'
    audiofile:
      description: 'Name of introductory audio file to play. MUST be WAV, pcm-s16le, mono, 16000 Hz.'
      example: 'picotts-beep.wav'
    message:
      description: 'The text message to speak (can be a template)'
      example: "It's {{ temperature }} degrees outside."
    language:
      description: 'The language to speak in (defaults to tts: setting)'
      example: 'en-GB'
  sequence:
    # only alert if "Audio Alerts" is on
    - condition: state
      entity_id: 'input_boolean.audio_alerts'
      state: 'on'
    # play text message
    - service: tts.picotts_say
      data_template:
        entity_id: "{{ entity_id | default(states('var.audio_alerts_media_player'),true) }}"
        language: "{{ language | default(states('input_select.audio_language'), true) }}"
        message: >
          {% set language = language | default(states('input_select.audio_language'), true) %}
          {% set lang = language[0:2] %}
          {% if audiofile == '' %}
          {% elif audiofile %}
            <play file="{{ states('var.audio_alerts_base_path') }}/{{ lang }}/{{ audiofile }}"/>
          {% else %}
            <play file="{{ states('var.audio_alerts_base_path') }}/{{ lang }}/picotts-beep.wav"/>
          {% endif %}
          <volume level="60">{{ message }}</volume>

This is actually my normal script.say that I also use in other places, like automations.

It uses the prerequisites mentioned above:

  • A well-defined folder structure for the audio files (because we don’t want to give the whole path all over, in case it changes).

  • The default audiofiles picotts-beep.wav and picotts-alert09.wav (RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 16000 Hz format) as “sound bits” to introduce a spoken TTS message.

  • The var: custom component, to store some defaults:

    • var.audio_alerts_media_player: The default media player’s entity ID.
    • var.audio_alerts_base_path: The (internal) path to audio files, for PicoTTS.

    My var: section looks like this:

    # Custom variables component (in "custom_components/var/")
    var:
      audio_alerts_media_player:
        friendly_name: "Default Media Player for Audio Alerts"
        initial_value: media_player.signalpi1
    
      audio_alerts_base_url:
        friendly_name: "Base URL for audio alert messages"
        initial_value: "https://has1.example.com/local/alerts"
    
      audio_alerts_base_path:
        friendly_name: "Base (internal) path for audio alerts"
        initial_value: "/home/homeassistant/.homeassistant/www/alerts"
    
  • A boolean input input_boolean.audio_alerts, to decide if we should talk at all:

    input_boolean:
      audio_alerts:
        name: Audio Alerts
        initial: on
        icon: mdi:voice
    
  • A select input input_select.audio_language, to select the output language.

    input_select:
      audio_language:
        name: 'Audio Language'
        # first 2 chars will be used as subfolder name for pre-recorded audio alerts
        # full name will be used as language input to PicoTTS, first two letters for Google TTS
        options:
          - 'en-GB'
          - 'de-DE'
    

You can of course change all that, the code isn’t very complicated.
Note I use a volume level of 60% for PicoTTS, because it tends to produce clipping WAV files otherwise,

Remember to restart HA after these changes.

Try it out!

Let’s try it out step-by-step:

Testing script.say

First, let’s check if our script.say works correctly—that is later used for the notifier.
I assume you’ve already created the audio files picotts-beep.wav and picotts-alert09.wav, for instance using a tool like Audacity, and put them in the correct places. For my installation, this would be:

www/
  alerts/
    de/
      picotts-beep.wav
      picotts-alert09.wav
      …
    en/
      picotts-beep.wav
      picotts-alert09.wav
      …

Remember: These files must be in the RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 16000 Hz format for PicoTTS!

Now go to Developer Tools → Services and try out the script.say (using your media player):

Play around with it and also check if the defaults are applied when leaving out things other than message:.

Testing notify.tts

Now let’s try if our newly made notify.tts service works correctly:

Again, you should be able to leave out language: and audiofile: and the defaults should be substituted.

Use notify.tts in an automation

Let’s say we wish to announce a new Home Assistant update in an automation.

Put this (or something similar) into the automation: section of your configuration.yaml (or in automations.yaml):

# A new update for HA is available
- alias: "Update available notification"
  trigger:
    - platform: state
      entity_id: binary_sensor.updater
      from: 'off'
      to: 'on'
  action:
    - service: notify.tts
      data_template:
        title: 'Update available:'
        message: >
          Home Assistant {{ state_attr('binary_sensor.updater', 'newest_version') }} is available.
          
          Check out the Release Notes at: {{ state_attr('binary_sensor.updater', 'release_notes') }}

We will now be notified via TTS whenever a new HA update is available!

Use notify groups

Hmm. We just found out that a purely spoken announcement isn’t so helpful: It might occur at night, when we’re not at home, or when TTS output has been switched off.

Remember notify groups? Looks like this would be the ideal place to add our spoken notification, because we already have a notify group notify.group_important.

Let’s make a new notify group notify.group_important_plus_audio! Under notify:, in configuration.yaml, add:

notify:

  # Notify group for important alerts
  - name: group_important
    platform: group
    services:
      - service: email
      - service: telegram

  # Notify group for important alerts, plus audio alert
  - name: group_important_plus_audio
    platform: group
    services:
      - service: group_important
      - service: tts

We now have two notify groups:

  • notify.group_important - uses notify.email and notify.telegram (adapt to your needs)
  • notify.group_important_plus_audio - uses whatever is in group_important plus our new notify.tts (or notify.tts_google)

Now let’s modify our automation above to use notify.group_important_plus_audio:

# A new update for HA is available
- alias: "Update available notification"
  trigger:
    - platform: state
      entity_id: binary_sensor.updater
      from: 'off'
      to: 'on'
  action:
    - service: notify.group_important_plus_audio
      data_template:
        title: 'Update available:'
        message: >
          Home Assistant {{ state_attr('binary_sensor.updater', 'newest_version') }} is available.
          
          Check out the Release Notes at: {{ state_attr('binary_sensor.updater', 'release_notes') }}

We now get notifications via email, Telegram and TTS whenever a HA upgrade is available!

(Remember to restart HA to make the changes effective.)

Use a TTS notifier in an alert

Home Assistant alerts and alarms use notifiers extensively. Let’s go and make a garbage day alert that uses our newly created TTS notifiers!

Under alert: in your configuration.yaml, add this (example assumes you also use use Telegram and have your bot set up—an entire different beast):

# Alerts
alert:

  garbage_day:
    name: 'Garbage Day'
    title: 'Reminder:'
    message: 'It is time to put the rubbish bin out.'
    #done_message: 'Rubbish bin put out'
    entity_id: binary_sensor.garbage_day
    state: 'on'
    repeat: 30
    can_acknowledge: true
    skip_first: false
    notifiers:
      - group_important_plus_audio
    data:
      # data for 'tts'
      language: 'en-GB'
      # data for 'telegram'
      inline_keyboard:
        # rows of buttons (array of arrays)
        - - ["Reminder off", "/ack garbage_day"]
          - ["Help", "/help garbage_day"]

This will set up an alert as soon as the garbage_day sensor switches to ‘on’, for every 30 minutes until acknowledged or garbage day is over.

Remember to restart HA after changing/adding alerts!

Notes:

  • We used the notify group group_important_plus_audio from above for this example. You could also use the new tts or tts_google notifiers.

  • For testing, you could also use a boolean input for triggering, say input_boolean.garbage_day_test:

    input_boolean:
      garbage_day_test:
        name: "Garbage Day Test"
    
  • I left the data: for Telegram intentionally in, to show that data for more than one notifier can be added with an alert. The notifiers usually don’t care about extra data included in the data: section, thus our notify.tts and notify.tts_google wouldn’t care about Telegram’s inline_keyboard: data, and Telegram wouldn’t care about the language: data for our TTS notifier.

What we learned

  • Creatively adding things that aren’t there in HA.
  • Structuring things helps a lot later on (like variables, folder structure, groups).
  • Reading documents and trying out things can take a long time. But saves even more time in the long run.
  • Fiddling with ‘code’ can be enormous fun!

Enjoy!

Oh well, and let me know about any bugs or interesting things you find!

12 Likes

Thanks for the super clear and informative guide, I do have some T.S questions.
I’ve used the first option with the google tts, tried to test the service and got this:

Failed to call service notify/tts_google. HTTPConnectionPool(host='MyHAIP', port=80): Max retries exceeded with url: /api/services/tts/google_translate_say (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0xa99d97d0>: Failed to establish a new connection: [Errno 111] Connection refused'))

what could be the cause?

Thanks again,

Urllib3 doesn’t want to connect to the resource given for the REST API call.

I don’t know if Google Translate TTS and the REST API work over http: connections and with the internal addresses. Could only ever try with my (external) https: adress, because my Home Assistant doesn’t expose http: (I’m using Let’s Encrypt Certificates).

  • You did create a long-lived token and used it, right?
  • And didn’t forget to put "Bearer " (Bearer plus a blank) in front?
  • You did include the protocol (http: or https:) in the resource: line?
  • You have an entry with platform: google_translate under the tts: section in your configuration.yaml and restarted HA afterwards so the API endpoint /api/services/tts/google_translate_say is valid?

If you use docker (HA Container or “Hassio”), there might be other problems involved, because the containers run in their own network. For instance, I can use has1 as an address in my network just fine (it gets resolved by the router DNS), but need to use something like has1.fritz.box for an internal address when using docker containers (so the address can be resolved within the docker container, because it doesn’t belong to the docker network).

If using https:, urllib3 also checks if the FQDN (no IP address!) verifies with the certificate presented, and refuses connection otherwise.

Maybe someone more knowledgeable can step in here—I only ever use HA Core.

Hi, thanks for the help. this is the config:
ha

I did create a Token, added bearer, did include the http,and have google translate under the tts header.

Looks good to me, except I don’t know if the Google Translate platform runs on http: and a local (private range) IP address.

Hint: Does your HA instance actually run on port 80 internally? It is usually on port 8123, in which case you’d need to use something like:

    resource: http://192.168.1.112:8123/api/services/tts/google_translate_say

So let’s diagnose step-by-step, leave out notify and REST for a moment, and try if the service itself works. Go to Developer Tools → Services and try the following (substituting your media player):

Screenshot_2020-07-22 Developer Tools - Home Assistant

If this works out, Google Translate TTS works ok and we can start investigating why the REST API call doesn’t.

Thank you for your post!

I got inspired by it and decided to create a more native way: https://github.com/home-assistant/core/pull/40028

Feedback is welcomed :slight_smile: