""" Support for API.AI webhook. For more details about this component, please refer to the documentation at https://home-assistant.io/components/apiai/ """ import asyncio import copy import logging import voluptuous as vol from homeassistant.const import PROJECT_NAME, HTTP_BAD_REQUEST from homeassistant.helpers import template, script, config_validation as cv from homeassistant.components.http import HomeAssistantView _LOGGER = logging.getLogger(__name__) INTENTS_API_ENDPOINT = '/api/apiai' CONF_INTENTS = 'intents' CONF_SPEECH = 'speech' CONF_ACTION = 'action' CONF_ASYNC_ACTION = 'async_action' DEFAULT_CONF_ASYNC_ACTION = False DOMAIN = 'apiai' DEPENDENCIES = ['http'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: { CONF_INTENTS: { cv.string: { vol.Optional(CONF_SPEECH): cv.template, vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ASYNC_ACTION, default=DEFAULT_CONF_ASYNC_ACTION): cv.boolean } } } }, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Activate API.AI component.""" intents = config[DOMAIN].get(CONF_INTENTS, {}) hass.http.register_view(ApiaiIntentsView(hass, intents)) return True class ApiaiIntentsView(HomeAssistantView): """Handle API.AI requests.""" url = INTENTS_API_ENDPOINT name = 'api:apiai' def __init__(self, hass, intents): """Initialize API.AI view.""" super().__init__() self.hass = hass intents = copy.deepcopy(intents) template.attach(hass, intents) for name, intent in intents.items(): if CONF_ACTION in intent: intent[CONF_ACTION] = script.Script( hass, intent[CONF_ACTION], "Apiai intent {}".format(name)) self.intents = intents @asyncio.coroutine def post(self, request): """Handle API.AI.""" data = yield from request.json() _LOGGER.debug("Received api.ai request: %s", data) req = data.get('result') if req is None: _LOGGER.error("Received invalid data from api.ai: %s", data) return self.json_message( "Expected result value not received", HTTP_BAD_REQUEST) action_incomplete = req['actionIncomplete'] if action_incomplete: return None # use intent to no mix HASS actions with this parameter intent = req.get('action') parameters = req.get('parameters') # contexts = req.get('contexts') response = ApiaiResponse(parameters) # Default Welcome Intent # Maybe is better to handle this in api.ai directly? # # if intent == 'input.welcome': # response.add_speech( # "Hello, and welcome to the future. How may I help?") # return self.json(response) if intent == "": _LOGGER.warning("Received intent with empty action") response.add_speech( "You have not defined an action in your api.ai intent.") return self.json(response) config = self.intents.get(intent) if config is None: _LOGGER.warning("Received unknown intent %s", intent) response.add_speech( "Intent '%s' is not yet configured within Home Assistant." % intent) return self.json(response) speech = config.get(CONF_SPEECH) action = config.get(CONF_ACTION) async_action = config.get(CONF_ASYNC_ACTION) if action is not None: # API.AI expects a response in less than 5s if async_action: # Do not wait for the action to be executed. # Needed if the action will take longer than 5s to execute self.hass.async_add_job(action.async_run(response.parameters)) else: # Wait for the action to be executed so we can use results to # render the answer yield from action.async_run(response.parameters) # pylint: disable=unsubscriptable-object if speech is not None: response.add_speech(speech) return self.json(response) class ApiaiResponse(object): """Help generating the response for API.AI.""" def __init__(self, parameters): """Initialize the response.""" self.speech = None self.parameters = {} # Parameter names replace '.' and '-' for '_' for key, value in parameters.items(): underscored_key = key.replace('.', '_').replace('-', '_') self.parameters[underscored_key] = value def add_speech(self, text): """Add speech to the response.""" assert self.speech is None if isinstance(text, template.Template): text = text.async_render(self.parameters) self.speech = text def as_dict(self): """Return response in an API.AI valid dict.""" return { 'speech': self.speech, 'displayText': self.speech, 'source': PROJECT_NAME, }