"""Support for Snips on-device ASR and NLU.""" import json import logging from datetime import timedelta import voluptuous as vol from homeassistant.core import callback from homeassistant.helpers import intent, config_validation as cv from homeassistant.components import mqtt DOMAIN = 'snips' CONF_INTENTS = 'intents' CONF_ACTION = 'action' CONF_FEEDBACK = 'feedback_sounds' CONF_PROBABILITY = 'probability_threshold' CONF_SITE_IDS = 'site_ids' SERVICE_SAY = 'say' SERVICE_SAY_ACTION = 'say_action' SERVICE_FEEDBACK_ON = 'feedback_on' SERVICE_FEEDBACK_OFF = 'feedback_off' INTENT_TOPIC = 'hermes/intent/#' FEEDBACK_ON_TOPIC = 'hermes/feedback/sound/toggleOn' FEEDBACK_OFF_TOPIC = 'hermes/feedback/sound/toggleOff' ATTR_TEXT = 'text' ATTR_SITE_ID = 'site_id' ATTR_CUSTOM_DATA = 'custom_data' ATTR_CAN_BE_ENQUEUED = 'can_be_enqueued' ATTR_INTENT_FILTER = 'intent_filter' _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_FEEDBACK): cv.boolean, vol.Optional(CONF_PROBABILITY, default=0): vol.Coerce(float), vol.Optional(CONF_SITE_IDS, default=['default']): vol.All(cv.ensure_list, [cv.string]), }), }, 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.Optional('value'): cv.match_all, vol.Optional('rawValue'): cv.match_all } }] }, extra=vol.ALLOW_EXTRA) SERVICE_SCHEMA_SAY = vol.Schema({ vol.Required(ATTR_TEXT): str, vol.Optional(ATTR_SITE_ID, default='default'): str, vol.Optional(ATTR_CUSTOM_DATA, default=''): str }) SERVICE_SCHEMA_SAY_ACTION = vol.Schema({ vol.Required(ATTR_TEXT): str, vol.Optional(ATTR_SITE_ID, default='default'): str, vol.Optional(ATTR_CUSTOM_DATA, default=''): str, vol.Optional(ATTR_CAN_BE_ENQUEUED, default=True): cv.boolean, vol.Optional(ATTR_INTENT_FILTER): vol.All(cv.ensure_list), }) SERVICE_SCHEMA_FEEDBACK = vol.Schema({ vol.Optional(ATTR_SITE_ID, default='default'): str }) async def async_setup(hass, config): """Activate Snips component.""" @callback def async_set_feedback(site_ids, state): """Set Feedback sound state.""" site_ids = (site_ids if site_ids else config[DOMAIN].get(CONF_SITE_IDS)) topic = (FEEDBACK_ON_TOPIC if state else FEEDBACK_OFF_TOPIC) for site_id in site_ids: payload = json.dumps({'siteId': site_id}) hass.components.mqtt.async_publish( FEEDBACK_ON_TOPIC, None, qos=0, retain=False) hass.components.mqtt.async_publish( topic, payload, qos=int(state), retain=state) if CONF_FEEDBACK in config[DOMAIN]: async_set_feedback(None, config[DOMAIN][CONF_FEEDBACK]) async def message_received(msg): """Handle new messages on MQTT.""" _LOGGER.debug("New intent: %s", msg.payload) try: request = json.loads(msg.payload) except TypeError: _LOGGER.error('Received invalid JSON: %s', msg.payload) return if (request['intent']['confidenceScore'] < config[DOMAIN].get(CONF_PROBABILITY)): _LOGGER.warning("Intent below probaility threshold %s < %s", request['intent']['confidenceScore'], config[DOMAIN].get(CONF_PROBABILITY)) return try: request = INTENT_SCHEMA(request) except vol.Invalid as err: _LOGGER.error('Intent has invalid schema: %s. %s', err, request) return if request['intent']['intentName'].startswith('user_'): intent_type = request['intent']['intentName'].split('__')[-1] else: intent_type = request['intent']['intentName'].split(':')[-1] snips_response = None slots = {} for slot in request.get('slots', []): slots[slot['slotName']] = {'value': resolve_slot_values(slot)} slots['{}_raw'.format(slot['slotName'])] = { 'value': slot['rawValue']} slots['site_id'] = {'value': request.get('siteId')} slots['session_id'] = {'value': request.get('sessionId')} slots['confidenceScore'] = { 'value': request['intent']['confidenceScore'] } try: intent_response = await intent.async_handle( hass, DOMAIN, intent_type, slots, request['input']) if 'plain' in intent_response.speech: snips_response = intent_response.speech['plain']['speech'] except intent.UnknownIntent: _LOGGER.warning("Received unknown intent %s", request['intent']['intentName']) except intent.IntentError: _LOGGER.exception("Error while handling intent: %s.", intent_type) if snips_response: notification = {'sessionId': request.get('sessionId', 'default'), 'text': snips_response} _LOGGER.debug("send_response %s", json.dumps(notification)) mqtt.async_publish(hass, 'hermes/dialogueManager/endSession', json.dumps(notification)) await hass.components.mqtt.async_subscribe( INTENT_TOPIC, message_received) async def snips_say(call): """Send a Snips notification message.""" notification = {'siteId': call.data.get(ATTR_SITE_ID, 'default'), 'customData': call.data.get(ATTR_CUSTOM_DATA, ''), 'init': {'type': 'notification', 'text': call.data.get(ATTR_TEXT)}} mqtt.async_publish(hass, 'hermes/dialogueManager/startSession', json.dumps(notification)) return async def snips_say_action(call): """Send a Snips action message.""" notification = {'siteId': call.data.get(ATTR_SITE_ID, 'default'), 'customData': call.data.get(ATTR_CUSTOM_DATA, ''), 'init': {'type': 'action', 'text': call.data.get(ATTR_TEXT), 'canBeEnqueued': call.data.get( ATTR_CAN_BE_ENQUEUED, True), 'intentFilter': call.data.get(ATTR_INTENT_FILTER, [])}} mqtt.async_publish(hass, 'hermes/dialogueManager/startSession', json.dumps(notification)) return async def feedback_on(call): """Turn feedback sounds on.""" async_set_feedback(call.data.get(ATTR_SITE_ID), True) async def feedback_off(call): """Turn feedback sounds off.""" async_set_feedback(call.data.get(ATTR_SITE_ID), False) hass.services.async_register( DOMAIN, SERVICE_SAY, snips_say, schema=SERVICE_SCHEMA_SAY) hass.services.async_register( DOMAIN, SERVICE_SAY_ACTION, snips_say_action, schema=SERVICE_SCHEMA_SAY_ACTION) hass.services.async_register( DOMAIN, SERVICE_FEEDBACK_ON, feedback_on, schema=SERVICE_SCHEMA_FEEDBACK) hass.services.async_register( DOMAIN, SERVICE_FEEDBACK_OFF, feedback_off, schema=SERVICE_SCHEMA_FEEDBACK) return True def resolve_slot_values(slot): """Convert snips builtin types to usable values.""" if 'value' in slot['value']: value = slot['value']['value'] else: value = slot['rawValue'] if slot.get('entity') == "snips/duration": delta = timedelta(weeks=slot['value']['weeks'], days=slot['value']['days'], hours=slot['value']['hours'], minutes=slot['value']['minutes'], seconds=slot['value']['seconds']) value = delta.seconds return value