From 825c91f0c3a3d3a672c97f29bb0d31188c2ed6ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Jan 2016 16:58:44 -0800 Subject: [PATCH] Add calling service functionality to Alexa --- homeassistant/components/alexa.py | 6 +++ .../components/automation/__init__.py | 23 ++------ homeassistant/helpers/service.py | 37 +++++++++++++ tests/components/test_alexa.py | 53 ++++++++++++++++++- 4 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 homeassistant/helpers/service.py diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 0b06f3c9a79..66ac9de0b43 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -11,6 +11,7 @@ import logging from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.util import template +from homeassistant.helpers.service import call_from_config DOMAIN = 'alexa' DEPENDENCIES = ['http'] @@ -23,6 +24,7 @@ API_ENDPOINT = '/api/alexa' CONF_INTENTS = 'intents' CONF_CARD = 'card' CONF_SPEECH = 'speech' +CONF_ACTION = 'action' def setup(hass, config): @@ -80,6 +82,7 @@ def _handle_alexa(handler, path_match, data): speech = config.get(CONF_SPEECH) card = config.get(CONF_CARD) + action = config.get(CONF_ACTION) # pylint: disable=unsubscriptable-object if speech is not None: @@ -89,6 +92,9 @@ def _handle_alexa(handler, path_match, data): response.add_card(CardType[card['type']], card['title'], card['content']) + if action is not None: + call_from_config(handler.server.hass, action, True) + handler.write_json(response.as_dict()) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 23d83f554ca..9c464f6954e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,9 +9,9 @@ https://home-assistant.io/components/automation/ import logging from homeassistant.bootstrap import prepare_setup_platform -from homeassistant.util import split_entity_id -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM from homeassistant.components import logbook +from homeassistant.helpers.service import call_from_config DOMAIN = 'automation' @@ -19,8 +19,6 @@ DEPENDENCIES = ['group'] CONF_ALIAS = 'alias' CONF_SERVICE = 'service' -CONF_SERVICE_ENTITY_ID = 'entity_id' -CONF_SERVICE_DATA = 'data' CONF_CONDITION = 'condition' CONF_ACTION = 'action' @@ -96,22 +94,7 @@ def _get_action(hass, config, name): _LOGGER.info('Executing %s', name) logbook.log_entry(hass, name, 'has been triggered', DOMAIN) - domain, service = split_entity_id(config[CONF_SERVICE]) - service_data = config.get(CONF_SERVICE_DATA, {}) - - if not isinstance(service_data, dict): - _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) - service_data = {} - - if CONF_SERVICE_ENTITY_ID in config: - try: - service_data[ATTR_ENTITY_ID] = \ - config[CONF_SERVICE_ENTITY_ID].split(",") - except AttributeError: - service_data[ATTR_ENTITY_ID] = \ - config[CONF_SERVICE_ENTITY_ID] - - hass.services.call(domain, service, service_data) + call_from_config(hass, config) return action diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py new file mode 100644 index 00000000000..632b3d39cbb --- /dev/null +++ b/homeassistant/helpers/service.py @@ -0,0 +1,37 @@ +"""Service calling related helpers.""" +import logging + +from homeassistant.util import split_entity_id +from homeassistant.const import ATTR_ENTITY_ID + +CONF_SERVICE = 'service' +CONF_SERVICE_ENTITY_ID = 'entity_id' +CONF_SERVICE_DATA = 'data' + +_LOGGER = logging.getLogger(__name__) + + +def call_from_config(hass, config, blocking=False): + """Call a service based on a config hash.""" + if CONF_SERVICE not in config: + _LOGGER.error('Missing key %s: %s', CONF_SERVICE, config) + return + + domain, service = split_entity_id(config[CONF_SERVICE]) + service_data = config.get(CONF_SERVICE_DATA) + + if service_data is None: + service_data = {} + elif isinstance(service_data, dict): + service_data = dict(service_data) + else: + _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) + service_data = {} + + entity_id = config.get(CONF_SERVICE_ENTITY_ID) + if isinstance(entity_id, str): + service_data[ATTR_ENTITY_ID] = entity_id.split(",") + elif entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(domain, service, service_data, blocking) diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index 741cfff4bb8..42acf5b3f62 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -27,12 +27,13 @@ API_URL = "http://127.0.0.1:{}{}".format(SERVER_PORT, alexa.API_ENDPOINT) HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD} hass = None +calls = [] @patch('homeassistant.components.http.util.get_local_ip', return_value='127.0.0.1') def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name - """ Initalizes a Home Assistant server. """ + """Initalize a Home Assistant server for testing this module.""" global hass hass = ha.HomeAssistant() @@ -42,6 +43,8 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, http.CONF_SERVER_PORT: SERVER_PORT}}) + hass.services.register('test', 'alexa', lambda call: calls.append(call)) + bootstrap.setup_component(hass, alexa.DOMAIN, { 'alexa': { 'intents': { @@ -61,7 +64,20 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name 'GetZodiacHoroscopeIntent': { 'speech': { 'type': 'plaintext', - 'text': 'You told us your sign is {{ ZodiacSign }}.' + 'text': 'You told us your sign is {{ ZodiacSign }}.', + } + }, + 'CallServiceIntent': { + 'speech': { + 'type': 'plaintext', + 'text': 'Service called', + }, + 'action': { + 'service': 'test.alexa', + 'data': { + 'hello': 1 + }, + 'entity_id': 'switch.test', } } } @@ -231,6 +247,39 @@ class TestAlexa(unittest.TestCase): text = req.json().get('response', {}).get('outputSpeech', {}).get('text') self.assertEqual('You are both home, you silly', text) + def test_intent_request_calling_service(self): + data = { + 'version': '1.0', + 'session': { + 'new': False, + 'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000', + 'application': { + 'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe' + }, + 'attributes': {}, + 'user': { + 'userId': 'amzn1.account.AM3B00000000000000000000000' + } + }, + 'request': { + 'type': 'IntentRequest', + 'requestId': ' amzn1.echo-api.request.0000000-0000-0000-0000-00000000000', + 'timestamp': '2015-05-13T12:34:56Z', + 'intent': { + 'name': 'CallServiceIntent', + } + } + } + call_count = len(calls) + req = _req(data) + self.assertEqual(200, req.status_code) + self.assertEqual(call_count + 1, len(calls)) + call = calls[-1] + self.assertEqual('test', call.domain) + self.assertEqual('alexa', call.service) + self.assertEqual(['switch.test'], call.data.get('entity_id')) + self.assertEqual(1, call.data.get('hello')) + def test_session_ended_request(self): data = { 'version': '1.0',