Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 106 additions & 60 deletions homeassistant/components/alexa/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@
import enum
import logging

from homeassistant.exceptions import HomeAssistantError
from homeassistant.core import callback
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import intent
from homeassistant.components import http
from homeassistant.util.decorator import Registry

from .const import DOMAIN, SYN_RESOLUTION_MATCH

INTENTS_API_ENDPOINT = '/api/alexa'

HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)


Expand Down Expand Up @@ -47,6 +48,10 @@ def async_setup(hass):
hass.http.register_view(AlexaIntentsView)


class UnknownRequest(HomeAssistantError):
"""When an unknown Alexa request is passed in."""


class AlexaIntentsView(http.HomeAssistantView):
"""Handle Alexa requests."""

Expand All @@ -57,71 +62,112 @@ class AlexaIntentsView(http.HomeAssistantView):
def post(self, request):
"""Handle Alexa."""
hass = request.app['hass']
data = yield from request.json()

_LOGGER.debug('Received Alexa request: %s', data)

req = data.get('request')

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)

req_type = req['type']

if req_type == 'SessionEndedRequest':
return None
message = yield from request.json()

alexa_intent_info = req.get('intent')
alexa_response = AlexaResponse(hass, alexa_intent_info)

if req_type != 'IntentRequest' and req_type != 'LaunchRequest':
_LOGGER.warning('Received unsupported request: %s', req_type)
return self.json_message(
'Received unsupported request: {}'.format(req_type),
HTTP_BAD_REQUEST)

if req_type == 'LaunchRequest':
intent_name = data.get('session', {}) \
.get('application', {}) \
.get('applicationId')
else:
intent_name = alexa_intent_info['name']
_LOGGER.debug('Received Alexa request: %s', message)

try:
intent_response = yield from intent.async_handle(
hass, DOMAIN, intent_name,
{key: {'value': value} for key, value
in alexa_response.variables.items()})
response = yield from async_handle_message(hass, message)
return b'' if response is None else self.json(response)
except UnknownRequest as err:
_LOGGER.warning(str(err))
return self.json(intent_error_response(
hass, message, str(err)))

except intent.UnknownIntent as err:
_LOGGER.warning('Received unknown intent %s', intent_name)
alexa_response.add_speech(
SpeechType.plaintext,
"This intent is not yet configured within Home Assistant.")
return self.json(alexa_response)
_LOGGER.warning(str(err))
return self.json(intent_error_response(
hass, message,
"This intent is not yet configured within Home Assistant."))

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)

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

if 'simple' in intent_response.card:
alexa_response.add_card(
CardType.simple, intent_response.card['simple']['title'],
intent_response.card['simple']['content'])

return self.json(alexa_response)
return self.json(intent_error_response(
hass, message,
"Invalid slot information received for this intent."))

except intent.IntentError as err:
_LOGGER.exception(str(err))
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."""
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()


@asyncio.coroutine
def async_handle_message(hass, message):
"""Handle an Alexa intent.

Raises:
- UnknownRequest
- intent.UnknownIntent
- intent.InvalidSlotInfo
- intent.IntentError
"""
req = message.get('request')
req_type = req['type']

handler = HANDLERS.get(req_type)

if not handler:
raise UnknownRequest('Received unknown request {}'.format(req_type))

return (yield from handler(hass, message))


@HANDLERS.register('SessionEndedRequest')
@asyncio.coroutine
def async_handle_session_end(hass, message):
"""Handle a session end request."""
return None


@HANDLERS.register('IntentRequest')
@HANDLERS.register('LaunchRequest')
@asyncio.coroutine
def async_handle_intent(hass, message):
"""Handle an intent request.

Raises:
- intent.UnknownIntent
- intent.InvalidSlotInfo
- intent.IntentError
"""
req = message.get('request')
alexa_intent_info = req.get('intent')
alexa_response = AlexaResponse(hass, alexa_intent_info)

if req['type'] == 'LaunchRequest':
intent_name = message.get('session', {}) \
.get('application', {}) \
.get('applicationId')
else:
intent_name = alexa_intent_info['name']

intent_response = yield from intent.async_handle(
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(
alexa_speech,
intent_response.speech[intent_speech]['speech'])
break

if 'simple' in intent_response.card:
alexa_response.add_card(
CardType.simple, intent_response.card['simple']['title'],
intent_response.card['simple']['content'])

return alexa_response.as_dict()


def resolve_slot_synonyms(key, request):
Expand Down
102 changes: 60 additions & 42 deletions homeassistant/components/dialogflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import voluptuous as vol

from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent, template
from homeassistant.components.http import HomeAssistantView

Expand All @@ -33,6 +33,10 @@
}, extra=vol.ALLOW_EXTRA)


class DialogFlowError(HomeAssistantError):
"""Raised when a DialogFlow error happens."""


@asyncio.coroutine
def async_setup(hass, config):
"""Set up Dialogflow component."""
Expand All @@ -51,57 +55,71 @@ class DialogflowIntentsView(HomeAssistantView):
def post(self, request):
"""Handle Dialogflow."""
hass = request.app['hass']
data = yield from request.json()
message = yield from request.json()

_LOGGER.debug("Received Dialogflow request: %s", data)
_LOGGER.debug("Received Dialogflow request: %s", message)

req = data.get('result')
try:
response = yield from async_handle_message(hass, message)
return b'' if response is None else self.json(response)

if req is None:
_LOGGER.error("Received invalid data from Dialogflow: %s", data)
return self.json_message(
"Expected result value not received", HTTP_BAD_REQUEST)
except DialogFlowError as err:
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message, str(err)))

action_incomplete = req['actionIncomplete']
except intent.UnknownIntent as err:
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message,
"This intent is not yet configured within Home Assistant."))

if action_incomplete:
return None
except intent.InvalidSlotInfo as err:
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message,
"Invalid slot information received for this intent."))

action = req.get('action')
parameters = req.get('parameters')
dialogflow_response = DialogflowResponse(parameters)
except intent.IntentError as err:
_LOGGER.warning(str(err))
return self.json(dialogflow_error_response(
hass, message, "Error handling intent."))

if action == "":
_LOGGER.warning("Received intent with empty action")
dialogflow_response.add_speech(
"You have not defined an action in your Dialogflow intent.")
return self.json(dialogflow_response)

try:
intent_response = yield from intent.async_handle(
hass, DOMAIN, action,
{key: {'value': value} for key, value
in parameters.items()})
def dialogflow_error_response(hass, message, error):
"""Return a response saying the error message."""
dialogflow_response = DialogflowResponse(message['result']['parameters'])
dialogflow_response.add_speech(error)
return dialogflow_response.as_dict()

except intent.UnknownIntent as err:
_LOGGER.warning("Received unknown intent %s", action)
dialogflow_response.add_speech(
"This intent is not yet configured within Home Assistant.")
return self.json(dialogflow_response)

except intent.InvalidSlotInfo as err:
_LOGGER.error("Received invalid slot data: %s", err)
return self.json_message('Invalid slot data received',
HTTP_BAD_REQUEST)
except intent.IntentError:
_LOGGER.exception("Error handling request for %s", action)
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)

if 'plain' in intent_response.speech:
dialogflow_response.add_speech(
intent_response.speech['plain']['speech'])

return self.json(dialogflow_response)
@asyncio.coroutine
def async_handle_message(hass, message):
"""Handle a DialogFlow message."""
req = message.get('result')
action_incomplete = req['actionIncomplete']

if action_incomplete:
return None

action = req.get('action', '')
parameters = req.get('parameters')
dialogflow_response = DialogflowResponse(parameters)

if action == "":
raise DialogFlowError(
"You have not defined an action in your Dialogflow intent.")

intent_response = yield from intent.async_handle(
hass, DOMAIN, action,
{key: {'value': value} for key, value
in parameters.items()})

if 'plain' in intent_response.speech:
dialogflow_response.add_speech(
intent_response.speech['plain']['speech'])

return dialogflow_response.as_dict()


class DialogflowResponse(object):
Expand Down
8 changes: 5 additions & 3 deletions homeassistant/helpers/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def async_handle(hass, platform, intent_type, slots=None, text_input=None):
handler = hass.data.get(DATA_KEY, {}).get(intent_type)

if handler is None:
raise UnknownIntent()
raise UnknownIntent('Unknown intent {}'.format(intent_type))

intent = Intent(hass, platform, intent_type, slots or {}, text_input)

Expand All @@ -50,9 +50,11 @@ def async_handle(hass, platform, intent_type, slots=None, text_input=None):
result = yield from handler.async_handle(intent)
return result
except vol.Invalid as err:
raise InvalidSlotInfo from err
raise InvalidSlotInfo(
'Received invalid slot info for {}'.format(intent_type)) from err
except Exception as err:
raise IntentHandleError from err
raise IntentHandleError(
'Error handling {}'.format(intent_type)) from err


class IntentError(HomeAssistantError):
Expand Down