2017-07-22 04:38:53 +00:00
|
|
|
"""Module to coordinate user intentions."""
|
|
|
|
import asyncio
|
|
|
|
import logging
|
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
|
|
|
from homeassistant.core import callback
|
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2017-10-08 15:17:54 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
2017-07-22 04:38:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
DATA_KEY = 'intent'
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
SLOT_SCHEMA = vol.Schema({
|
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
SPEECH_TYPE_PLAIN = 'plain'
|
|
|
|
SPEECH_TYPE_SSML = 'ssml'
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2017-07-22 04:38:53 +00:00
|
|
|
def async_register(hass, handler):
|
|
|
|
"""Register an intent with Home Assistant."""
|
|
|
|
intents = hass.data.get(DATA_KEY)
|
|
|
|
if intents is None:
|
|
|
|
intents = hass.data[DATA_KEY] = {}
|
|
|
|
|
|
|
|
if handler.intent_type in intents:
|
|
|
|
_LOGGER.warning('Intent %s is being overwritten by %s.',
|
|
|
|
handler.intent_type, handler)
|
|
|
|
|
|
|
|
intents[handler.intent_type] = handler
|
|
|
|
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2017-07-22 04:38:53 +00:00
|
|
|
def async_handle(hass, platform, intent_type, slots=None, text_input=None):
|
|
|
|
"""Handle an intent."""
|
|
|
|
handler = hass.data.get(DATA_KEY, {}).get(intent_type)
|
|
|
|
|
|
|
|
if handler is None:
|
|
|
|
raise UnknownIntent()
|
|
|
|
|
|
|
|
intent = Intent(hass, platform, intent_type, slots or {}, text_input)
|
|
|
|
|
|
|
|
try:
|
|
|
|
_LOGGER.info("Triggering intent handler %s", handler)
|
|
|
|
result = yield from handler.async_handle(intent)
|
|
|
|
return result
|
|
|
|
except vol.Invalid as err:
|
|
|
|
raise InvalidSlotInfo from err
|
|
|
|
except Exception as err:
|
|
|
|
raise IntentHandleError from err
|
|
|
|
|
|
|
|
|
|
|
|
class IntentError(HomeAssistantError):
|
|
|
|
"""Base class for intent related errors."""
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class UnknownIntent(IntentError):
|
|
|
|
"""When the intent is not registered."""
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidSlotInfo(IntentError):
|
|
|
|
"""When the slot data is invalid."""
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class IntentHandleError(IntentError):
|
|
|
|
"""Error while handling intent."""
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class IntentHandler:
|
|
|
|
"""Intent handler registration."""
|
|
|
|
|
|
|
|
intent_type = None
|
|
|
|
slot_schema = None
|
|
|
|
_slot_schema = None
|
|
|
|
platforms = None
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_can_handle(self, intent_obj):
|
|
|
|
"""Test if an intent can be handled."""
|
|
|
|
return self.platforms is None or intent_obj.platform in self.platforms
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_validate_slots(self, slots):
|
|
|
|
"""Validate slot information."""
|
|
|
|
if self.slot_schema is None:
|
|
|
|
return slots
|
|
|
|
|
|
|
|
if self._slot_schema is None:
|
|
|
|
self._slot_schema = vol.Schema({
|
|
|
|
key: SLOT_SCHEMA.extend({'value': validator})
|
|
|
|
for key, validator in self.slot_schema.items()})
|
|
|
|
|
|
|
|
return self._slot_schema(slots)
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_handle(self, intent_obj):
|
|
|
|
"""Handle the intent."""
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
"""String representation of intent handler."""
|
|
|
|
return '<{} - {}>'.format(self.__class__.__name__, self.intent_type)
|
|
|
|
|
|
|
|
|
|
|
|
class Intent:
|
|
|
|
"""Hold the intent."""
|
|
|
|
|
|
|
|
__slots__ = ['hass', 'platform', 'intent_type', 'slots', 'text_input']
|
|
|
|
|
|
|
|
def __init__(self, hass, platform, intent_type, slots, text_input):
|
|
|
|
"""Initialize an intent."""
|
|
|
|
self.hass = hass
|
|
|
|
self.platform = platform
|
|
|
|
self.intent_type = intent_type
|
|
|
|
self.slots = slots
|
|
|
|
self.text_input = text_input
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def create_response(self):
|
|
|
|
"""Create a response."""
|
|
|
|
return IntentResponse(self)
|
|
|
|
|
|
|
|
|
|
|
|
class IntentResponse:
|
|
|
|
"""Response to an intent."""
|
|
|
|
|
2017-07-25 07:42:59 +00:00
|
|
|
def __init__(self, intent=None):
|
2017-07-22 04:38:53 +00:00
|
|
|
"""Initialize an IntentResponse."""
|
|
|
|
self.intent = intent
|
|
|
|
self.speech = {}
|
|
|
|
self.card = {}
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_set_speech(self, speech, speech_type='plain', extra_data=None):
|
|
|
|
"""Set speech response."""
|
|
|
|
self.speech[speech_type] = {
|
|
|
|
'speech': speech,
|
|
|
|
'extra_data': extra_data,
|
|
|
|
}
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_set_card(self, title, content, card_type='simple'):
|
|
|
|
"""Set speech response."""
|
|
|
|
self.card[card_type] = {
|
|
|
|
'title': title,
|
|
|
|
'content': content,
|
|
|
|
}
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def as_dict(self):
|
|
|
|
"""Return a dictionary representation of an intent response."""
|
|
|
|
return {
|
|
|
|
'speech': self.speech,
|
|
|
|
'card': self.card,
|
|
|
|
}
|