2015-12-13 06:29:02 +00:00
|
|
|
"""
|
2016-02-23 20:06:50 +00:00
|
|
|
Support for Alexa skill service end point.
|
2015-12-13 06:29:02 +00:00
|
|
|
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/alexa/
|
|
|
|
"""
|
2016-10-24 06:48:01 +00:00
|
|
|
import asyncio
|
2015-12-13 06:29:02 +00:00
|
|
|
import enum
|
|
|
|
import logging
|
2016-09-28 04:29:55 +00:00
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
from homeassistant.core import callback
|
2016-05-14 07:58:36 +00:00
|
|
|
from homeassistant.const import HTTP_BAD_REQUEST
|
2017-09-12 19:24:44 +00:00
|
|
|
from homeassistant.helpers import intent
|
2017-07-22 04:38:53 +00:00
|
|
|
from homeassistant.components import http
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-09-12 19:24:44 +00:00
|
|
|
from .const import DOMAIN
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2016-10-13 16:14:22 +00:00
|
|
|
INTENTS_API_ENDPOINT = '/api/alexa'
|
|
|
|
|
2017-09-12 19:24:44 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2015-12-13 06:29:02 +00:00
|
|
|
|
|
|
|
|
2016-09-28 04:29:55 +00:00
|
|
|
class SpeechType(enum.Enum):
|
|
|
|
"""The Alexa speech types."""
|
|
|
|
|
|
|
|
plaintext = "PlainText"
|
|
|
|
ssml = "SSML"
|
|
|
|
|
|
|
|
|
2017-07-22 04:38:53 +00:00
|
|
|
SPEECH_MAPPINGS = {
|
|
|
|
'plain': SpeechType.plaintext,
|
|
|
|
'ssml': SpeechType.ssml,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-09-28 04:29:55 +00:00
|
|
|
class CardType(enum.Enum):
|
|
|
|
"""The Alexa card types."""
|
|
|
|
|
|
|
|
simple = "Simple"
|
|
|
|
link_account = "LinkAccount"
|
|
|
|
|
|
|
|
|
2017-09-12 19:24:44 +00:00
|
|
|
@callback
|
|
|
|
def async_setup(hass):
|
2016-02-23 20:06:50 +00:00
|
|
|
"""Activate Alexa component."""
|
2017-07-22 04:38:53 +00:00
|
|
|
hass.http.register_view(AlexaIntentsView)
|
2016-04-23 05:10:57 +00:00
|
|
|
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-07-22 04:38:53 +00:00
|
|
|
class AlexaIntentsView(http.HomeAssistantView):
|
2016-05-14 07:58:36 +00:00
|
|
|
"""Handle Alexa requests."""
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2016-10-13 16:14:22 +00:00
|
|
|
url = INTENTS_API_ENDPOINT
|
2016-05-14 07:58:36 +00:00
|
|
|
name = 'api:alexa'
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@asyncio.coroutine
|
2016-05-14 07:58:36 +00:00
|
|
|
def post(self, request):
|
|
|
|
"""Handle Alexa."""
|
2017-07-22 04:38:53 +00:00
|
|
|
hass = request.app['hass']
|
2016-10-24 06:48:01 +00:00
|
|
|
data = yield from request.json()
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2016-05-14 07:58:36 +00:00
|
|
|
_LOGGER.debug('Received Alexa request: %s', data)
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2016-05-14 07:58:36 +00:00
|
|
|
req = data.get('request')
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2016-05-14 07:58:36 +00:00
|
|
|
if req is None:
|
|
|
|
_LOGGER.error('Received invalid data from Alexa: %s', data)
|
|
|
|
return self.json_message('Expected request value not received',
|
|
|
|
HTTP_BAD_REQUEST)
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2016-05-14 07:58:36 +00:00
|
|
|
req_type = req['type']
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2016-05-14 07:58:36 +00:00
|
|
|
if req_type == 'SessionEndedRequest':
|
|
|
|
return None
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-07-22 04:38:53 +00:00
|
|
|
alexa_intent_info = req.get('intent')
|
|
|
|
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-08-02 05:53:36 +00:00
|
|
|
if req_type != 'IntentRequest' and req_type != 'LaunchRequest':
|
2016-05-14 07:58:36 +00:00
|
|
|
_LOGGER.warning('Received unsupported request: %s', req_type)
|
|
|
|
return self.json_message(
|
|
|
|
'Received unsupported request: {}'.format(req_type),
|
|
|
|
HTTP_BAD_REQUEST)
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-08-02 05:53:36 +00:00
|
|
|
if req_type == 'LaunchRequest':
|
|
|
|
intent_name = data.get('session', {}) \
|
|
|
|
.get('application', {}) \
|
|
|
|
.get('applicationId')
|
|
|
|
else:
|
|
|
|
intent_name = alexa_intent_info['name']
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-07-22 04:38:53 +00:00
|
|
|
try:
|
|
|
|
intent_response = yield from intent.async_handle(
|
|
|
|
hass, DOMAIN, intent_name,
|
|
|
|
{key: {'value': value} for key, value
|
|
|
|
in alexa_response.variables.items()})
|
|
|
|
except intent.UnknownIntent as err:
|
2016-05-14 07:58:36 +00:00
|
|
|
_LOGGER.warning('Received unknown intent %s', intent_name)
|
2017-07-22 04:38:53 +00:00
|
|
|
alexa_response.add_speech(
|
2016-05-14 07:58:36 +00:00
|
|
|
SpeechType.plaintext,
|
|
|
|
"This intent is not yet configured within Home Assistant.")
|
2017-07-22 04:38:53 +00:00
|
|
|
return self.json(alexa_response)
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-07-22 04:38:53 +00:00
|
|
|
except intent.InvalidSlotInfo as err:
|
|
|
|
_LOGGER.error('Received invalid slot data from Alexa: %s', err)
|
|
|
|
return self.json_message('Invalid slot data received',
|
|
|
|
HTTP_BAD_REQUEST)
|
|
|
|
except intent.IntentError:
|
|
|
|
_LOGGER.exception('Error handling request for %s', intent_name)
|
|
|
|
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
|
2016-06-12 00:57:04 +00:00
|
|
|
|
2017-07-22 04:38:53 +00:00
|
|
|
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
|
|
|
|
if intent_speech in intent_response.speech:
|
|
|
|
alexa_response.add_speech(
|
|
|
|
alexa_speech,
|
|
|
|
intent_response.speech[intent_speech]['speech'])
|
|
|
|
break
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-07-22 04:38:53 +00:00
|
|
|
if 'simple' in intent_response.card:
|
|
|
|
alexa_response.add_card(
|
2017-07-30 04:52:26 +00:00
|
|
|
CardType.simple, intent_response.card['simple']['title'],
|
2017-07-22 04:38:53 +00:00
|
|
|
intent_response.card['simple']['content'])
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-07-22 04:38:53 +00:00
|
|
|
return self.json(alexa_response)
|
2015-12-13 06:29:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
class AlexaResponse(object):
|
2016-03-08 16:55:57 +00:00
|
|
|
"""Help generating the response for Alexa."""
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-07-22 04:38:53 +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
|
2016-12-29 16:26:23 +00:00
|
|
|
self.variables = {}
|
2017-07-22 04:38:53 +00:00
|
|
|
# Intent is None if request was a LaunchRequest or SessionEndedRequest
|
|
|
|
if intent_info is not None:
|
|
|
|
for key, value in intent_info.get('slots', {}).items():
|
2017-11-10 17:35:57 +00:00
|
|
|
underscored_key = key.replace('.', '_')
|
|
|
|
|
2016-12-29 16:26:23 +00:00
|
|
|
if 'value' in value:
|
|
|
|
self.variables[underscored_key] = value['value']
|
2015-12-13 06:29:02 +00:00
|
|
|
|
2017-11-10 17:35:57 +00:00
|
|
|
if 'resolutions' in value:
|
|
|
|
self._populate_resolved_values(underscored_key, value)
|
|
|
|
|
|
|
|
def _populate_resolved_values(self, underscored_key, value):
|
|
|
|
for resolution in value['resolutions']['resolutionsPerAuthority']:
|
|
|
|
if 'values' not in resolution:
|
|
|
|
continue
|
|
|
|
|
|
|
|
for resolved in resolution['values']:
|
|
|
|
if 'value' not in resolved:
|
|
|
|
continue
|
|
|
|
|
2017-11-14 06:46:26 +00:00
|
|
|
if 'name' in resolved['value']:
|
2017-11-10 17:35:57 +00:00
|
|
|
self.variables[underscored_key] = resolved['value']['name']
|
|
|
|
|
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
|
|
|
|
|
|
|
|
card = {
|
|
|
|
"type": card_type.value
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
|
|
|
|
|
|
|
self.speech = {
|
|
|
|
'type': speech_type.value,
|
2016-09-28 04:29:55 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
|
|
|
|
|
|
|
self.reprompt = {
|
|
|
|
'type': speech_type.value,
|
2016-10-24 06:48:01 +00:00
|
|
|
key: text.async_render(self.variables)
|
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."""
|
2015-12-13 06:29:02 +00:00
|
|
|
response = {
|
|
|
|
'shouldEndSession': self.should_end_session
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.card is not None:
|
|
|
|
response['card'] = self.card
|
|
|
|
|
|
|
|
if self.speech is not None:
|
|
|
|
response['outputSpeech'] = self.speech
|
|
|
|
|
|
|
|
if self.reprompt is not None:
|
|
|
|
response['reprompt'] = {
|
|
|
|
'outputSpeech': self.reprompt
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
'version': '1.0',
|
|
|
|
'sessionAttributes': self.session_attributes,
|
|
|
|
'response': response,
|
|
|
|
}
|