core/homeassistant/components/alexa/intent.py

295 lines
8.5 KiB
Python
Raw Normal View History

"""Support for Alexa skill service end point."""
2015-12-13 06:29:02 +00:00
import enum
import logging
from homeassistant.components import http
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent
from homeassistant.util.decorator import Registry
2015-12-13 06:29:02 +00:00
from .const import DOMAIN, SYN_RESOLUTION_MATCH
2015-12-13 06:29:02 +00:00
_LOGGER = logging.getLogger(__name__)
2015-12-13 06:29:02 +00:00
HANDLERS = Registry() # type: ignore[var-annotated]
2019-07-31 19:25:30 +00:00
INTENTS_API_ENDPOINT = "/api/alexa"
2015-12-13 06:29:02 +00:00
class SpeechType(enum.Enum):
"""The Alexa speech types."""
2019-07-31 19:25:30 +00:00
plaintext = "PlainText"
ssml = "SSML"
2019-07-31 19:25:30 +00:00
SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml}
class CardType(enum.Enum):
"""The Alexa card types."""
2019-07-31 19:25:30 +00:00
simple = "Simple"
link_account = "LinkAccount"
@callback
def async_setup(hass):
2016-02-23 20:06:50 +00:00
"""Activate Alexa component."""
hass.http.register_view(AlexaIntentsView)
2016-04-23 05:10:57 +00:00
2015-12-13 06:29:02 +00:00
async def async_setup_intents(hass):
"""
Do intents setup.
Right now this module does not expose any, but the intent component breaks
without it.
"""
pass # pylint: disable=unnecessary-pass
class UnknownRequest(HomeAssistantError):
"""When an unknown Alexa request is passed in."""
class AlexaIntentsView(http.HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""Handle Alexa requests."""
2015-12-13 06:29:02 +00:00
url = INTENTS_API_ENDPOINT
2019-07-31 19:25:30 +00:00
name = "api:alexa"
2016-05-14 07:58:36 +00:00
async def post(self, request):
2016-05-14 07:58:36 +00:00
"""Handle Alexa."""
2019-07-31 19:25:30 +00:00
hass = request.app["hass"]
message = await request.json()
2015-12-13 06:29:02 +00:00
_LOGGER.debug("Received Alexa request: %s", message)
2015-12-13 06:29:02 +00:00
try:
response = await async_handle_message(hass, message)
2019-07-31 19:25:30 +00:00
return b"" if response is None else self.json(response)
except UnknownRequest as err:
_LOGGER.warning(str(err))
2019-07-31 19:25:30 +00:00
return self.json(intent_error_response(hass, message, str(err)))
except intent.UnknownIntent as err:
_LOGGER.warning(str(err))
2019-07-31 19:25:30 +00:00
return self.json(
intent_error_response(
hass,
message,
"This intent is not yet configured within Home Assistant.",
)
)
2015-12-13 06:29:02 +00:00
except intent.InvalidSlotInfo as err:
_LOGGER.error("Received invalid slot data from Alexa: %s", err)
2019-07-31 19:25:30 +00:00
return self.json(
intent_error_response(
hass, message, "Invalid slot information received for this intent."
)
)
except intent.IntentError as err:
_LOGGER.exception(str(err))
2019-07-31 19:25:30 +00:00
return self.json(
intent_error_response(hass, message, "Error handling intent.")
)
def intent_error_response(hass, message, error):
"""Return an Alexa response that will speak the error message."""
2019-07-31 19:25:30 +00:00
alexa_intent_info = message.get("request").get("intent")
alexa_response = AlexaResponse(hass, alexa_intent_info)
alexa_response.add_speech(SpeechType.plaintext, error)
return alexa_response.as_dict()
async def async_handle_message(hass, message):
"""Handle an Alexa intent.
Raises:
- UnknownRequest
- intent.UnknownIntent
- intent.InvalidSlotInfo
- intent.IntentError
"""
2019-07-31 19:25:30 +00:00
req = message.get("request")
req_type = req["type"]
2021-10-17 18:19:56 +00:00
if not (handler := HANDLERS.get(req_type)):
raise UnknownRequest(f"Received unknown request {req_type}")
return await handler(hass, message)
2019-07-31 19:25:30 +00:00
@HANDLERS.register("SessionEndedRequest")
async def async_handle_session_end(hass, message):
"""Handle a session end request."""
return None
2019-07-31 19:25:30 +00:00
@HANDLERS.register("IntentRequest")
@HANDLERS.register("LaunchRequest")
async def async_handle_intent(hass, message):
"""Handle an intent request.
Raises:
- intent.UnknownIntent
- intent.InvalidSlotInfo
- intent.IntentError
"""
2019-07-31 19:25:30 +00:00
req = message.get("request")
alexa_intent_info = req.get("intent")
alexa_response = AlexaResponse(hass, alexa_intent_info)
2019-07-31 19:25:30 +00:00
if req["type"] == "LaunchRequest":
intent_name = (
message.get("session", {}).get("application", {}).get("applicationId")
)
else:
2019-07-31 19:25:30 +00:00
intent_name = alexa_intent_info["name"]
intent_response = await intent.async_handle(
2019-07-31 19:25:30 +00:00
hass,
DOMAIN,
intent_name,
{key: {"value": value} for key, value in alexa_response.variables.items()},
)
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
if intent_speech in intent_response.speech:
alexa_response.add_speech(
2019-07-31 19:25:30 +00:00
alexa_speech, intent_response.speech[intent_speech]["speech"]
)
2022-01-31 18:23:26 +00:00
if intent_speech in intent_response.reprompt:
alexa_response.add_reprompt(
alexa_speech, intent_response.reprompt[intent_speech]["reprompt"]
)
2019-07-31 19:25:30 +00:00
if "simple" in intent_response.card:
alexa_response.add_card(
2019-07-31 19:25:30 +00:00
CardType.simple,
intent_response.card["simple"]["title"],
intent_response.card["simple"]["content"],
)
return alexa_response.as_dict()
2015-12-13 06:29:02 +00:00
def resolve_slot_synonyms(key, request):
"""Check slot request for synonym resolutions."""
# Default to the spoken slot value if more than one or none are found. For
# reference to the request object structure, see the Alexa docs:
# https://tinyurl.com/ybvm7jhs
2019-07-31 19:25:30 +00:00
resolved_value = request["value"]
2019-07-31 19:25:30 +00:00
if (
"resolutions" in request
and "resolutionsPerAuthority" in request["resolutions"]
and len(request["resolutions"]["resolutionsPerAuthority"]) >= 1
):
# Extract all of the possible values from each authority with a
# successful match
possible_values = []
2019-07-31 19:25:30 +00:00
for entry in request["resolutions"]["resolutionsPerAuthority"]:
if entry["status"]["code"] != SYN_RESOLUTION_MATCH:
continue
2019-07-31 19:25:30 +00:00
possible_values.extend([item["value"]["name"] for item in entry["values"]])
# If there is only one match use the resolved value, otherwise the
# resolution cannot be determined, so use the spoken slot value
if len(possible_values) == 1:
resolved_value = possible_values[0]
else:
_LOGGER.debug(
2019-07-31 19:25:30 +00:00
"Found multiple synonym resolutions for slot value: {%s: %s}",
key,
resolved_value,
)
return resolved_value
class AlexaResponse:
2016-03-08 16:55:57 +00:00
"""Help generating the response for Alexa."""
2015-12-13 06:29:02 +00:00
def __init__(self, hass, intent_info):
2016-03-08 16:55:57 +00:00
"""Initialize the response."""
2015-12-13 06:29:02 +00:00
self.hass = hass
self.speech = None
self.card = None
self.reprompt = None
self.session_attributes = {}
self.should_end_session = True
self.variables = {}
# Intent is None if request was a LaunchRequest or SessionEndedRequest
if intent_info is not None:
2019-07-31 19:25:30 +00:00
for key, value in intent_info.get("slots", {}).items():
# Only include slots with values
2019-07-31 19:25:30 +00:00
if "value" not in value:
continue
2019-07-31 19:25:30 +00:00
_key = key.replace(".", "_")
self.variables[_key] = resolve_slot_synonyms(key, value)
2015-12-13 06:29:02 +00:00
def add_card(self, card_type, title, content):
2016-03-08 16:55:57 +00:00
"""Add a card to the response."""
2015-12-13 06:29:02 +00:00
assert self.card is None
2019-07-31 19:25:30 +00:00
card = {"type": card_type.value}
2015-12-13 06:29:02 +00:00
if card_type == CardType.link_account:
self.card = card
return
2017-07-30 04:52:26 +00:00
card["title"] = title
card["content"] = content
2015-12-13 06:29:02 +00:00
self.card = card
def add_speech(self, speech_type, text):
2016-03-08 16:55:57 +00:00
"""Add speech to the response."""
2015-12-13 06:29:02 +00:00
assert self.speech is None
2019-07-31 19:25:30 +00:00
key = "ssml" if speech_type == SpeechType.ssml else "text"
2015-12-13 06:29:02 +00:00
2019-07-31 19:25:30 +00:00
self.speech = {"type": speech_type.value, key: text}
2015-12-13 06:29:02 +00:00
def add_reprompt(self, speech_type, text):
2016-02-23 20:06:50 +00:00
"""Add reprompt if user does not answer."""
2015-12-13 06:29:02 +00:00
assert self.reprompt is None
2019-07-31 19:25:30 +00:00
key = "ssml" if speech_type == SpeechType.ssml else "text"
2015-12-13 06:29:02 +00:00
2022-01-31 18:23:26 +00:00
self.should_end_session = False
self.reprompt = {"type": speech_type.value, key: text}
2015-12-13 06:29:02 +00:00
def as_dict(self):
2016-03-08 16:55:57 +00:00
"""Return response in an Alexa valid dict."""
2019-07-31 19:25:30 +00:00
response = {"shouldEndSession": self.should_end_session}
2015-12-13 06:29:02 +00:00
if self.card is not None:
2019-07-31 19:25:30 +00:00
response["card"] = self.card
2015-12-13 06:29:02 +00:00
if self.speech is not None:
2019-07-31 19:25:30 +00:00
response["outputSpeech"] = self.speech
2015-12-13 06:29:02 +00:00
if self.reprompt is not None:
2019-07-31 19:25:30 +00:00
response["reprompt"] = {"outputSpeech": self.reprompt}
2015-12-13 06:29:02 +00:00
return {
2019-07-31 19:25:30 +00:00
"version": "1.0",
"sessionAttributes": self.session_attributes,
"response": response,
2015-12-13 06:29:02 +00:00
}