123 lines
3.3 KiB
Python
123 lines
3.3 KiB
Python
"""
|
|
Support for Snips on-device ASR and NLU.
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
https://home-assistant.io/components/snips/
|
|
"""
|
|
import asyncio
|
|
import copy
|
|
import json
|
|
import logging
|
|
import voluptuous as vol
|
|
from homeassistant.helpers import template, script, config_validation as cv
|
|
import homeassistant.loader as loader
|
|
|
|
DOMAIN = 'snips'
|
|
DEPENDENCIES = ['mqtt']
|
|
CONF_INTENTS = 'intents'
|
|
CONF_ACTION = 'action'
|
|
|
|
INTENT_TOPIC = 'hermes/nlu/intentParsed'
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
DOMAIN: {
|
|
CONF_INTENTS: {
|
|
cv.string: {
|
|
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
|
}
|
|
}
|
|
}
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
INTENT_SCHEMA = vol.Schema({
|
|
vol.Required('input'): str,
|
|
vol.Required('intent'): {
|
|
vol.Required('intentName'): str
|
|
},
|
|
vol.Optional('slots'): [{
|
|
vol.Required('slotName'): str,
|
|
vol.Required('value'): {
|
|
vol.Required('kind'): str,
|
|
vol.Required('value'): cv.match_all
|
|
}
|
|
}]
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
@asyncio.coroutine
|
|
def async_setup(hass, config):
|
|
"""Activate Snips component."""
|
|
mqtt = loader.get_component('mqtt')
|
|
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
|
handler = IntentHandler(hass, intents)
|
|
|
|
@asyncio.coroutine
|
|
def message_received(topic, payload, qos):
|
|
"""Handle new messages on MQTT."""
|
|
LOGGER.debug("New intent: %s", payload)
|
|
yield from handler.handle_intent(payload)
|
|
|
|
yield from mqtt.async_subscribe(hass, INTENT_TOPIC, message_received)
|
|
|
|
return True
|
|
|
|
|
|
class IntentHandler(object):
|
|
"""Help handling intents."""
|
|
|
|
def __init__(self, hass, intents):
|
|
"""Initialize the intent handler."""
|
|
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], "Snips intent {}".format(name))
|
|
|
|
self.intents = intents
|
|
|
|
@asyncio.coroutine
|
|
def handle_intent(self, payload):
|
|
"""Handle an intent."""
|
|
try:
|
|
response = json.loads(payload)
|
|
except TypeError:
|
|
LOGGER.error('Received invalid JSON: %s', payload)
|
|
return
|
|
|
|
try:
|
|
response = INTENT_SCHEMA(response)
|
|
except vol.Invalid as err:
|
|
LOGGER.error('Intent has invalid schema: %s. %s', err, response)
|
|
return
|
|
|
|
intent = response['intent']['intentName'].split('__')[-1]
|
|
config = self.intents.get(intent)
|
|
|
|
if config is None:
|
|
LOGGER.warning("Received unknown intent %s. %s", intent, response)
|
|
return
|
|
|
|
action = config.get(CONF_ACTION)
|
|
|
|
if action is not None:
|
|
slots = self.parse_slots(response)
|
|
yield from action.async_run(slots)
|
|
|
|
# pylint: disable=no-self-use
|
|
def parse_slots(self, response):
|
|
"""Parse the intent slots."""
|
|
parameters = {}
|
|
|
|
for slot in response.get('slots', []):
|
|
key = slot['slotName']
|
|
value = slot['value']['value']
|
|
if value is not None:
|
|
parameters[key] = value
|
|
|
|
return parameters
|