diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 3ade199aabb..8283b563591 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -9,15 +9,16 @@ import asyncio 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__) @@ -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.""" @@ -57,71 +62,112 @@ class AlexaIntentsView(http.HomeAssistantView): def post(self, request): """Handle Alexa.""" hass = request.app['hass'] - data = yield from request.json() + message = 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 - - 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) + return self.json(intent_error_response( + hass, message, + "Invalid slot information received for this intent.")) - 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 + except intent.IntentError as err: + _LOGGER.exception(str(err)) + return self.json(intent_error_response( + hass, message, "Error handling intent.")) - 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) +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): diff --git a/homeassistant/components/dialogflow.py b/homeassistant/components/dialogflow.py index 726b8d99e01..63205c5479c 100644 --- a/homeassistant/components/dialogflow.py +++ b/homeassistant/components/dialogflow.py @@ -9,7 +9,7 @@ import logging 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 @@ -33,6 +33,10 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) +class DialogFlowError(HomeAssistantError): + """Raised when a DialogFlow error happens.""" + + @asyncio.coroutine def async_setup(hass, config): """Set up Dialogflow component.""" @@ -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) - - req = data.get('result') - - 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) - - action_incomplete = req['actionIncomplete'] - - if action_incomplete: - return None - - action = req.get('action') - parameters = req.get('parameters') - dialogflow_response = DialogflowResponse(parameters) - - 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) + _LOGGER.debug("Received Dialogflow request: %s", message) try: - intent_response = yield from intent.async_handle( - hass, DOMAIN, action, - {key: {'value': value} for key, value - in parameters.items()}) + response = yield from async_handle_message(hass, message) + return b'' if response is None else self.json(response) + + except DialogFlowError as err: + _LOGGER.warning(str(err)) + return self.json(dialogflow_error_response( + hass, message, str(err))) 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) + _LOGGER.warning(str(err)) + return self.json(dialogflow_error_response( + hass, message, + "This intent is not yet configured within Home Assistant.")) 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) + _LOGGER.warning(str(err)) + return self.json(dialogflow_error_response( + hass, message, + "Invalid slot information received for this intent.")) - if 'plain' in intent_response.speech: - dialogflow_response.add_speech( - intent_response.speech['plain']['speech']) + except intent.IntentError as err: + _LOGGER.warning(str(err)) + return self.json(dialogflow_error_response( + hass, message, "Error handling intent.")) - return self.json(dialogflow_response) + +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() + + +@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): diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index c5aad3ababc..6268b3cb9f7 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -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) @@ -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):