diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 03c1d24184a..522939a213a 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -364,6 +364,38 @@ abode: description: Entity id of the quick action to trigger. example: 'binary_sensor.home_quick_action' +snips: + say: + description: Send a TTS message to Snips. + fields: + text: + description: Text to say. + example: My name is snips + site_id: + description: Site to use to start session, defaults to default (optional) + example: bedroom + custom_data: + description: custom data that will be included with all messages in this session + example: user=UserName + say_action: + description: Send a TTS message to Snips to listen for a response. + fields: + text: + description: Text to say + example: My name is snips + site_id: + description: Site to use to start session, defaults to default (optional) + example: bedroom + custom_data: + description: custom data that will be included with all messages in this session + example: user=UserName + can_be_enqueued: + description: If True, session waits for an open session to end, if False session is dropped if one is running + example: True + intent_filter: + description: Optional Array of Strings - A list of intents names to restrict the NLU resolution to on the first query. + example: turnOnLights, turnOffLights + input_boolean: toggle: description: Toggles an input boolean. diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index ae387f7ab4c..d221c8512c6 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -8,17 +8,29 @@ import asyncio import json import logging from datetime import timedelta + import voluptuous as vol + from homeassistant.helpers import intent, config_validation as cv import homeassistant.components.mqtt as mqtt DOMAIN = 'snips' DEPENDENCIES = ['mqtt'] + CONF_INTENTS = 'intents' CONF_ACTION = 'action' +SERVICE_SAY = 'say' +SERVICE_SAY_ACTION = 'say_action' + INTENT_TOPIC = 'hermes/intent/#' +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({ @@ -40,6 +52,20 @@ INTENT_SCHEMA = vol.Schema({ }] }, 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), +}) + @asyncio.coroutine def async_setup(hass, config): @@ -93,6 +119,39 @@ def async_setup(hass, config): yield from hass.components.mqtt.async_subscribe( INTENT_TOPIC, message_received) + @asyncio.coroutine + 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 + + @asyncio.coroutine + 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 + + 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) + return True diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py index 9ee500bb4c7..711d13dc341 100644 --- a/tests/components/test_snips.py +++ b/tests/components/test_snips.py @@ -4,7 +4,10 @@ import json from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component -from tests.common import async_fire_mqtt_message, async_mock_intent +from tests.common import (async_fire_mqtt_message, async_mock_intent, + async_mock_service) +from homeassistant.components.snips import (SERVICE_SCHEMA_SAY, + SERVICE_SCHEMA_SAY_ACTION) @asyncio.coroutine @@ -238,3 +241,66 @@ def test_snips_intent_username(hass, mqtt_mock): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'Lights' + + +@asyncio.coroutine +def test_snips_say(hass, caplog): + """Test snips say with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say', + SERVICE_SCHEMA_SAY) + + data = {'text': 'Hello'} + yield from hass.services.async_call('snips', 'say', data) + yield from hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'snips' + assert calls[0].service == 'say' + assert calls[0].data['text'] == 'Hello' + + +@asyncio.coroutine +def test_snips_say_action(hass, caplog): + """Test snips say_action with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say_action', + SERVICE_SCHEMA_SAY_ACTION) + + data = {'text': 'Hello', 'intent_filter': ['myIntent']} + yield from hass.services.async_call('snips', 'say_action', data) + yield from hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'snips' + assert calls[0].service == 'say_action' + assert calls[0].data['text'] == 'Hello' + assert calls[0].data['intent_filter'] == ['myIntent'] + + +@asyncio.coroutine +def test_snips_say_invalid_config(hass, caplog): + """Test snips say with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say', + SERVICE_SCHEMA_SAY) + + data = {'text': 'Hello', 'badKey': 'boo'} + yield from hass.services.async_call('snips', 'say', data) + yield from hass.async_block_till_done() + + assert len(calls) == 0 + assert 'ERROR' in caplog.text + assert 'Invalid service data' in caplog.text + + +@asyncio.coroutine +def test_snips_say_action_invalid_config(hass, caplog): + """Test snips say_action with invalid config.""" + calls = async_mock_service(hass, 'snips', 'say_action', + SERVICE_SCHEMA_SAY_ACTION) + + data = {'text': 'Hello', 'can_be_enqueued': 'notabool'} + yield from hass.services.async_call('snips', 'say_action', data) + yield from hass.async_block_till_done() + + assert len(calls) == 0 + assert 'ERROR' in caplog.text + assert 'Invalid service data' in caplog.text