Custom sentences and intents - the basics
To use intent scripts you have to install the intent script integration.
Custom sentences and intents come in pairs, linked by an intent name.
language: "en"
intents:
CustomWhatTime:
data:
- sentences:
- "what time is it"
- "tell me the time"
CustomWhatTime:
action:
- action: tts.speak
target:
entity_id: tts.home_assistant_cloud
data:
cache: true
media_player_entity_id: media_player.kitchen
message: Too late
Custom sentences should be saved as yaml files in config/custom_sentences/<language_code>/
with a separate file for each sentence. Being in English, the examples here are saved in config/custom_sentences/en/
. The name of the yaml file doesn't matter, but they should all begin with
language: "en"
intents:
<IntentName>:
where <IntentName>
is the name of the intent the sentence points to.
Intent scripts are stored together in configuration.yaml
, under the key intent_script:
. Their number will grow rapidly so it is good practice to keep them in a yaml file of their own, then add this to configuration.yaml
with the line:
intent_script: !include intents.yaml
For more information, see splitting configuration.yaml.
Entities in intent scripts do not have to be exposed to Assist as they do in built-in sentences. On the other hand, any aliases you have set up will not be available - these have to be provided by the custom sentence. In this example radio station names are aliases for Sonos Favourites IDs:
language: "en"
intents:
CustomRadioPlay:
data:
- sentences:
- "Play {station}"
lists:
station:
values:
- in: "radio one"
out: "FV:2/87"
- in: "radio 1"
out: "FV:2/87"
- in: "radio two"
out: "FV:2/88"
- in: "radio 2"
out: "FV:2/88"
- in: "radio too"
out: "FV:2/88"
Sentences
Syntax
Complete custom sentences can be listed
- sentences:
- "what time is it"
- "what is the time"
- "what's the time"
- "tell me the time"
...or a single sentence can be marked to show (alternative words), and [words which can be omitted]:
- sentences:
- "(what | what's | what is | tell me) [the] time [is it]"
Documentation on custom sentence syntax can be found here.
Slots
Slots are placeholders in custom sentences containing values to be passed to the intent script. In the sentence
- "play {album} by {artist}"
{album}
and {artist}
are slots. When a user says "Play Aqualung by Jethro Tull", the slot album
captures "Aqualung" and the slot artist
captures "Jethro Tull". The matched intent would be able to generate the TTS sentence
"OK. Playing {{album}} by {{artist}}"
If your slot value is a string, you can restrict the user to a list of valid options
language: en
intents:
PlayAlbum:
data:
- sentences:
- "play {album} by jethro tull"
lists:
album:
values:
- "Stand Up"
- "Aqualung"
- "Songs from the Wood"
- "Heavy Horses"
...or you can use a wildcard
lists:
album:
wildcard: true
Note: If you specify a list of options and the user provides a value not on the list, you will get the default error message from your voice assistant - "Sorry, I don't understand" or something similar. If you specify a wildcard, it's up to you to use the intent script to filter out invalid options and provide an error message.
If the user is likely to use a word that is different from the string you want to pass to the intent, you can specify "in" and "out" values. For example Start the { kitchen } timer
could pass on the entity ID of the timer to be started. As mentioned earlier, it can also create aliases, allowing users to refer to the same timer entity with different names.
lists:
timer:
values:
- in: "kitchen"
out: "timer.kitchen"
- in: "cooker"
out: "timer.kitchen"
- in: "egg"
out: "timer.kitchen"
If your slot value is a number, you can specify the range.
lists:
brightness:
range:
from: 1
to: 255
"Collisions"
In theory custom sentences should take precedence over built-in sentences, but this doesn't always happen. It's a particular problem when the custom sentence contains the words "in" or "on" - "What's in the diary?" is likely to give the error "No area named diary".
You can reduce the chances of this by creating a unique sentence first, then adding the problem sentence as an alternative. For example:
language: "en"
intents:
CustomCalendarToday:
data:
- sentences:
- "(What's | What is) happening"
- "(What's | What is) in the (calendar | diary)"
Intents
Intent scripts, like normal scripts, are series of actions to be executed. They are likely to contain more templating than normal scripts, and the last action will probably be a TTS statement of some kind.
If your custom sentence is a command, the first part of the intent will execute it and the second part will be TTS confirmation. For example:
language: "en"
intents:
CustomSetTemperature:
data:
- sentences:
- "(set|change) [the] (temperature | heating | thermostat) to {temp} [degrees]"
lists:
temp:
range:
from: 0
to: 35
CustomSetTemperature:
action:
- action: climate.set_temperature
data:
temperature: "{{ temp }}"
target:
entity_id: climate.hallway
- action: script.tts_response
data:
tts_sentence: "Heating set to {{ temp }}"
When the sentence a is question it gets more complicated. In this example, the user has asked "Have I got coffee on the shopping list?" "Coffee" is held in the slot {item}
.
QueryShoppingListItem:
action:
- action: todo.get_items
target:
entity_id: todo.shopping_list
data:
status: needs_action
response_variable: shopping_list_data
- variables:
found: "{{ shopping_list_data['todo.shopping_list']['items']|selectattr('summary','search',item)|list|count > 0 }}"
- action: script.tts_response
data:
tts_sentence: >-
{% if found %}
"Yes. {{item}} is already there."
{% else %}
"No. Can't find {{item}}"
{% endif %}
-
The first action is a standard todo list command, which finds incomplete items in the shopping list and lists them in the variable
shopping_list_data
. More on response variables here. -
The second action creates the variable
found
which will be true if coffee is inshopping_list_data
, false if not. -
The third action calls a script to deliver the TTS response, which will be "yes" if
found
is true, otherwise "no".
Note: in TTS statements and HA commands the slot value
item
is enclosed in {{curly brackets}}, marking the value as output. In the template the value is being processed, so there are no curly brackets. More details here.
In the previous example the if... else...
structure was in the TTS statement. You can also use the choose:
action to specify different paths with different actions.
DeleteShoppingListItem:
action:
- action: todo.get_items
target:
entity_id: todo.shopping_list
data:
status: needs_action
response_variable: shopping_list_data
- variables:
found: "{{ shopping_list_data['todo.shopping_list']['items']|selectattr('summary','search',item)|list|count > 0 }}"
- choose:
- conditions: "{{ found }}"
sequence:
- action: shopping_list.remove_item
data:
name: "{{ item }}"
- action: script.tts_response
data:
tts_sentence: "Removed {{ item }}"
- conditions: "{{ not found }}"
sequence:
- action: script.tts_response
data:
tts_sentence: "Sorry. Can't find {{ item }} in the list"
Variables
Variables in an intent script are local, so they don't exist outside it. If you want to pass a value on to another script, then that script will have to have a field defined to receive it.
fields:
variable_field:
selector:
text: null
name: tts_sentence
In the intent, call the second script with the variable as data:
- action: script.tts_response
data:
tts_sentence: "{{ variable }}"
Slot values
Slot values passed from the custom sentence can be used as variables throughout the intent script. If you want to carry out mathematical operations on them, bear in mind that in Jinja2 they will be strings, even if they were defined by the custom sentence as a range of numbers. They will have to be converted to numerical values using either | float(0)
or | int(0)
. In this example, hour
was defined in the custom sentence as a number in the range 1-12:
hour_val: >-
{% if minute | int(0) > 30 %}
{{ hour | int(0) -1 }}
{% else %}
{{ hour | int(0) }}
{% endif %}
Variables action
You can use the variables:
action to create any variables you need in your intent script.
action:
- variables:
hour_val: >-
{% if minute | int(0) > 30 %}
{{ hour | int(0) -1 }}
{% else %}
{{ hour | int(0) }}
{% endif %}
time: >-
{% if now().strftime('%H') | int(0) > 12 %}
{{ now().strftime('%H') | int(0) - 12 }}
{% else %}
{{ now().strftime('%H') | int(0) }}
{% endif %}
now_val: "{{ time | float(0) * 3600 + now().strftime('%M') | int(0) * 60 + now().strftime('%S') | int(0) }}"
alarm_val: "{{ hour_val | int(0) * 3600 + minute | int(0) * 60 }}"
alarm: >-
{% if alarm_val | int(0) < now_val | int(0) %}
{{ alarm_val | int(0) + 43200 - now_val | int(0) }}
{% else %}
{{ alarm_val | int(0) - now_val | int(0) }}
{% endif %}
- action: timer.start
target:
entity_id: timer.voice_alarm
data:
duration: "{{ alarm }}"
Response variables
response_variable:
in an intent script holds the output of an action and can be given any name you lke. In this example, the todo.get_items
action returns a list of incomplete items on a shopping list and stores it in shopping_list_data
.
- action: todo.get_items
target:
entity_id: todo.shopping_list
data:
status: needs_action
response_variable: shopping_list_data
Action response variables
action_response:
variables have a similar function, but they are only used in speech:
templates. They are always referenced by action_response
speech:
text: "{{ action_response['calendar.my_calendar'].events | length }}"