1 """Support for Alexa skill service end point."""
3 from collections.abc
import Callable, Coroutine
8 from aiohttp.web
import Response
16 from .const
import DOMAIN, SYN_RESOLUTION_MATCH
18 _LOGGER = logging.getLogger(__name__)
21 str, Callable[[HomeAssistant, dict[str, Any]], Coroutine[Any, Any, dict[str, Any]]]
24 INTENTS_API_ENDPOINT =
"/api/alexa"
28 """The Alexa speech types."""
30 plaintext =
"PlainText"
34 SPEECH_MAPPINGS = {
"plain": SpeechType.plaintext,
"ssml": SpeechType.ssml}
38 """The Alexa card types."""
41 link_account =
"LinkAccount"
46 """Activate Alexa component."""
47 hass.http.register_view(AlexaIntentsView)
53 Right now this module does not expose any, but the intent component breaks
59 """When an unknown Alexa request is passed in."""
62 class AlexaIntentsView(http.HomeAssistantView):
63 """Handle Alexa requests."""
65 url = INTENTS_API_ENDPOINT
68 async
def post(self, request: http.HomeAssistantRequest) -> Response | bytes:
70 hass = request.app[http.KEY_HASS]
71 message: dict[str, Any] = await request.json()
73 _LOGGER.debug(
"Received Alexa request: %s", message)
77 return b
"" if response
is None else self.json(response)
78 except UnknownRequest
as err:
79 _LOGGER.warning(
str(err))
82 except intent.UnknownIntent
as err:
83 _LOGGER.warning(
str(err))
88 "This intent is not yet configured within Home Assistant.",
92 except intent.InvalidSlotInfo
as err:
93 _LOGGER.error(
"Received invalid slot data from Alexa: %s", err)
96 hass, message,
"Invalid slot information received for this intent."
100 except intent.IntentError:
101 _LOGGER.exception(
"Error handling intent")
108 hass: HomeAssistant, message: dict[str, Any], error: str
110 """Return an Alexa response that will speak the error message."""
111 alexa_intent_info = message[
"request"].
get(
"intent")
113 alexa_response.add_speech(SpeechType.plaintext, error)
114 return alexa_response.as_dict()
118 hass: HomeAssistant, message: dict[str, Any]
120 """Handle an Alexa intent.
124 - intent.UnknownIntent
125 - intent.InvalidSlotInfo
129 req = message[
"request"]
130 req_type = req[
"type"]
132 if not (handler := HANDLERS.get(req_type)):
135 return await handler(hass, message)
138 @HANDLERS.register("SessionEndedRequest")
139 @HANDLERS.register("IntentRequest")
140 @HANDLERS.register("LaunchRequest")
142 hass: HomeAssistant, message: dict[str, Any]
144 """Handle an intent request.
147 - intent.UnknownIntent
148 - intent.InvalidSlotInfo
152 req = message[
"request"]
153 alexa_intent_info = req.get(
"intent")
156 if req[
"type"] ==
"LaunchRequest":
158 message.get(
"session", {}).
get(
"application", {}).
get(
"applicationId")
160 elif req[
"type"] ==
"SessionEndedRequest":
161 app_id = message.get(
"session", {}).
get(
"application", {}).
get(
"applicationId")
162 intent_name = f
"{app_id}.{req['type']}"
163 alexa_response.variables[
"reason"] = req[
"reason"]
164 alexa_response.variables[
"error"] = req.get(
"error")
166 intent_name = alexa_intent_info[
"name"]
168 intent_response = await intent.async_handle(
172 {key: {
"value": value}
for key, value
in alexa_response.variables.items()},
175 for intent_speech, alexa_speech
in SPEECH_MAPPINGS.items():
176 if intent_speech
in intent_response.speech:
177 alexa_response.add_speech(
178 alexa_speech, intent_response.speech[intent_speech][
"speech"]
180 if intent_speech
in intent_response.reprompt:
181 alexa_response.add_reprompt(
182 alexa_speech, intent_response.reprompt[intent_speech][
"reprompt"]
185 if "simple" in intent_response.card:
186 alexa_response.add_card(
188 intent_response.card[
"simple"][
"title"],
189 intent_response.card[
"simple"][
"content"],
192 return alexa_response.as_dict()
196 """Check slot request for synonym resolutions."""
201 resolved_data: dict[str, Any] = {}
202 resolved_data[
"value"] = request[
"value"]
203 resolved_data[
"id"] =
""
206 "resolutions" in request
207 and "resolutionsPerAuthority" in request[
"resolutions"]
208 and len(request[
"resolutions"][
"resolutionsPerAuthority"]) >= 1
214 for entry
in request[
"resolutions"][
"resolutionsPerAuthority"]:
215 if entry[
"status"][
"code"] != SYN_RESOLUTION_MATCH:
218 possible_values.extend([item[
"value"]
for item
in entry[
"values"]])
221 if len(possible_values) >= 1:
223 if "id" in possible_values[0]:
224 resolved_data[
"id"] = possible_values[0][
"id"]
228 if len(possible_values) == 1:
229 resolved_data[
"value"] = possible_values[0][
"name"]
232 "Found multiple synonym resolutions for slot value: {%s: %s}",
234 resolved_data[
"value"],
241 """Help generating the response for Alexa."""
243 def __init__(self, hass: HomeAssistant, intent_info: dict[str, Any] |
None) ->
None:
244 """Initialize the response."""
246 self.
speechspeech: dict[str, Any] |
None =
None
247 self.
cardcard: dict[str, Any] |
None =
None
248 self.
repromptreprompt: dict[str, Any] |
None =
None
249 self.session_attributes: dict[str, Any] = {}
251 self.variables: dict[str, Any] = {}
254 if intent_info
is not None:
255 for key, value
in intent_info.get(
"slots", {}).items():
257 if "value" not in value:
260 _key = key.replace(
".",
"_")
263 self.variables[_key] = _slot_data[
"value"]
264 self.variables[_key +
"_Id"] = _slot_data[
"id"]
266 def add_card(self, card_type: CardType, title: str, content: str) ->
None:
267 """Add a card to the response."""
268 assert self.
cardcard
is None
270 card = {
"type": card_type.value}
272 if card_type == CardType.link_account:
276 card[
"title"] = title
277 card[
"content"] = content
280 def add_speech(self, speech_type: SpeechType, text: str) ->
None:
281 """Add speech to the response."""
282 assert self.
speechspeech
is None
284 key =
"ssml" if speech_type == SpeechType.ssml
else "text"
286 self.
speechspeech = {
"type": speech_type.value, key: text}
289 """Add reprompt if user does not answer."""
290 assert self.
repromptreprompt
is None
292 key =
"ssml" if speech_type == SpeechType.ssml
else "text"
296 self.
repromptreprompt = {
"type": speech_type.value, key: text}
299 """Return response in an Alexa valid dict."""
300 response: dict[str, Any] = {
"shouldEndSession": self.
should_end_sessionshould_end_session}
302 if self.
cardcard
is not None:
303 response[
"card"] = self.
cardcard
305 if self.
speechspeech
is not None:
306 response[
"outputSpeech"] = self.
speechspeech
308 if self.
repromptreprompt
is not None:
309 response[
"reprompt"] = {
"outputSpeech": self.
repromptreprompt}
313 "sessionAttributes": self.session_attributes,
314 "response": response,
None add_speech(self, SpeechType speech_type, str text)
None __init__(self, HomeAssistant hass, dict[str, Any]|None intent_info)
None add_card(self, CardType card_type, str title, str content)
None add_reprompt(self, SpeechType speech_type, str text)
dict[str, Any] as_dict(self)
Response|bytes post(self, http.HomeAssistantRequest request)
dict[str, str] resolve_slot_data(str key, dict[str, Any] request)
dict[str, Any] intent_error_response(HomeAssistant hass, dict[str, Any] message, str error)
dict[str, Any] async_handle_intent(HomeAssistant hass, dict[str, Any] message)
dict[str, Any] async_handle_message(HomeAssistant hass, dict[str, Any] message)
None async_setup_intents(HomeAssistant hass)
None async_setup(HomeAssistant hass)
web.Response get(self, web.Request request, str config_key)