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/
|
|
|
|
"""
|
|
|
|
import enum
|
|
|
|
import logging
|
|
|
|
|
|
|
|
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
2016-01-09 00:58:44 +00:00
|
|
|
from homeassistant.helpers.service import call_from_config
|
2016-02-23 20:06:50 +00:00
|
|
|
from homeassistant.helpers import template
|
2015-12-13 06:29:02 +00:00
|
|
|
|
|
|
|
DOMAIN = 'alexa'
|
|
|
|
DEPENDENCIES = ['http']
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
_CONFIG = {}
|
|
|
|
|
|
|
|
API_ENDPOINT = '/api/alexa'
|
|
|
|
|
|
|
|
CONF_INTENTS = 'intents'
|
|
|
|
CONF_CARD = 'card'
|
|
|
|
CONF_SPEECH = 'speech'
|
2016-01-09 00:58:44 +00:00
|
|
|
CONF_ACTION = 'action'
|
2015-12-13 06:29:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
def setup(hass, config):
|
2016-02-23 20:06:50 +00:00
|
|
|
"""Activate Alexa component."""
|
2015-12-13 06:29:02 +00:00
|
|
|
_CONFIG.update(config[DOMAIN].get(CONF_INTENTS, {}))
|
|
|
|
|
|
|
|
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_alexa(handler, path_match, data):
|
2016-02-23 20:06:50 +00:00
|
|
|
"""Handle Alexa."""
|
2015-12-13 06:29:02 +00:00
|
|
|
_LOGGER.debug('Received Alexa request: %s', data)
|
|
|
|
|
|
|
|
req = data.get('request')
|
|
|
|
|
|
|
|
if req is None:
|
|
|
|
_LOGGER.error('Received invalid data from Alexa: %s', data)
|
|
|
|
handler.write_json_message(
|
|
|
|
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
|
|
|
return
|
|
|
|
|
|
|
|
req_type = req['type']
|
|
|
|
|
|
|
|
if req_type == 'SessionEndedRequest':
|
|
|
|
handler.send_response(HTTP_OK)
|
|
|
|
handler.end_headers()
|
|
|
|
return
|
|
|
|
|
|
|
|
intent = req.get('intent')
|
|
|
|
response = AlexaResponse(handler.server.hass, intent)
|
|
|
|
|
|
|
|
if req_type == 'LaunchRequest':
|
|
|
|
response.add_speech(
|
|
|
|
SpeechType.plaintext,
|
|
|
|
"Hello, and welcome to the future. How may I help?")
|
|
|
|
handler.write_json(response.as_dict())
|
|
|
|
return
|
|
|
|
|
|
|
|
if req_type != 'IntentRequest':
|
|
|
|
_LOGGER.warning('Received unsupported request: %s', req_type)
|
|
|
|
return
|
|
|
|
|
|
|
|
intent_name = intent['name']
|
|
|
|
config = _CONFIG.get(intent_name)
|
|
|
|
|
|
|
|
if config is None:
|
|
|
|
_LOGGER.warning('Received unknown intent %s', intent_name)
|
|
|
|
response.add_speech(
|
|
|
|
SpeechType.plaintext,
|
|
|
|
"This intent is not yet configured within Home Assistant.")
|
|
|
|
handler.write_json(response.as_dict())
|
|
|
|
return
|
|
|
|
|
|
|
|
speech = config.get(CONF_SPEECH)
|
|
|
|
card = config.get(CONF_CARD)
|
2016-01-09 00:58:44 +00:00
|
|
|
action = config.get(CONF_ACTION)
|
2015-12-13 06:29:02 +00:00
|
|
|
|
|
|
|
# pylint: disable=unsubscriptable-object
|
|
|
|
if speech is not None:
|
|
|
|
response.add_speech(SpeechType[speech['type']], speech['text'])
|
|
|
|
|
|
|
|
if card is not None:
|
|
|
|
response.add_card(CardType[card['type']], card['title'],
|
|
|
|
card['content'])
|
|
|
|
|
2016-01-09 00:58:44 +00:00
|
|
|
if action is not None:
|
|
|
|
call_from_config(handler.server.hass, action, True)
|
|
|
|
|
2015-12-13 06:29:02 +00:00
|
|
|
handler.write_json(response.as_dict())
|
|
|
|
|
|
|
|
|
|
|
|
class SpeechType(enum.Enum):
|
2016-03-08 16:55:57 +00:00
|
|
|
"""The Alexa speech types."""
|
|
|
|
|
2015-12-13 06:29:02 +00:00
|
|
|
plaintext = "PlainText"
|
|
|
|
ssml = "SSML"
|
|
|
|
|
|
|
|
|
|
|
|
class CardType(enum.Enum):
|
2016-03-08 16:55:57 +00:00
|
|
|
"""The Alexa card types."""
|
|
|
|
|
2015-12-13 06:29:02 +00:00
|
|
|
simple = "Simple"
|
|
|
|
link_account = "LinkAccount"
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
def __init__(self, hass, intent=None):
|
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
|
|
|
|
if intent is not None and 'slots' in intent:
|
|
|
|
self.variables = {key: value['value'] for key, value
|
2015-12-22 10:08:46 +00:00
|
|
|
in intent['slots'].items() if 'value' in value}
|
2015-12-13 06:29:02 +00:00
|
|
|
else:
|
|
|
|
self.variables = {}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
card["title"] = self._render(title),
|
|
|
|
card["content"] = self._render(content)
|
|
|
|
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,
|
|
|
|
key: self._render(text)
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
key: self._render(text)
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
|
|
|
def _render(self, template_string):
|
2016-02-23 20:06:50 +00:00
|
|
|
"""Render a response, adding data from intent if available."""
|
2015-12-13 06:29:02 +00:00
|
|
|
return template.render(self.hass, template_string, self.variables)
|