From 81e5a852f0d243f8e7d526fd9ab8e7c1e3e261f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Thu, 10 Mar 2016 21:36:05 +0100 Subject: [PATCH] Use templates in service calls --- .../components/automation/__init__.py | 8 ++-- homeassistant/helpers/service.py | 41 ++++++++++++++++--- tests/helpers/test_service.py | 37 +++++++++++++++++ 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index cff61829f19..1a2219a309c 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -11,13 +11,14 @@ from homeassistant.bootstrap import prepare_setup_platform from homeassistant.const import CONF_PLATFORM from homeassistant.components import logbook from homeassistant.helpers.service import call_from_config +from homeassistant.helpers.service import validate_service_call + DOMAIN = 'automation' DEPENDENCIES = ['group'] CONF_ALIAS = 'alias' -CONF_SERVICE = 'service' CONF_CONDITION = 'condition' CONF_ACTION = 'action' @@ -82,8 +83,9 @@ def _setup_automation(hass, config_block, name, config): def _get_action(hass, config, name): """Return an action based on a configuration.""" - if CONF_SERVICE not in config: - _LOGGER.error('Error setting up %s, no action specified.', name) + validation_error = validate_service_call(config) + if validation_error: + _LOGGER.error(validation_error) return None def action(): diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 2d198910408..83025978b8d 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -3,14 +3,16 @@ import functools import logging from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.helpers.entity import split_entity_id +from homeassistant.helpers import template from homeassistant.loader import get_component HASS = None CONF_SERVICE = 'service' +CONF_SERVICE_TEMPLATE = 'service_template' CONF_SERVICE_ENTITY_ID = 'entity_id' CONF_SERVICE_DATA = 'data' +CONF_SERVICE_DATA_TEMPLATE = 'data_template' _LOGGER = logging.getLogger(__name__) @@ -29,14 +31,20 @@ def service(domain, service_name): def call_from_config(hass, config, blocking=False): """Call a service based on a config hash.""" - if not isinstance(config, dict) or CONF_SERVICE not in config: - _LOGGER.error('Missing key %s: %s', CONF_SERVICE, config) + validation_error = validate_service_call(config) + if validation_error: + _LOGGER.error(validation_error) return + domain_service = ( + config[CONF_SERVICE] + if CONF_SERVICE in config + else template.render(hass, config[CONF_SERVICE_TEMPLATE])) + try: - domain, service_name = split_entity_id(config[CONF_SERVICE]) + domain, service_name = domain_service.split('.', 1) except ValueError: - _LOGGER.error('Invalid service specified: %s', config[CONF_SERVICE]) + _LOGGER.error('Invalid service specified: %s', domain_service) return service_data = config.get(CONF_SERVICE_DATA) @@ -49,6 +57,13 @@ def call_from_config(hass, config, blocking=False): _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) service_data = {} + service_data_template = config.get(CONF_SERVICE_DATA_TEMPLATE) + if service_data_template and isinstance(service_data_template, dict): + for key, value in service_data_template.items(): + service_data[key] = template.render(hass, value) + elif service_data_template: + _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) + entity_id = config.get(CONF_SERVICE_ENTITY_ID) if isinstance(entity_id, str): service_data[ATTR_ENTITY_ID] = [ent.strip() for ent in @@ -76,3 +91,19 @@ def extract_entity_ids(hass, service_call): return group.expand_entity_ids(hass, [service_ent_id]) return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)] + + +def validate_service_call(config): + """Validate service call configuration. + + Helper method to validate that a configuration is a valid service call. + Returns None if validation succeeds, else an error description + """ + if not isinstance(config, dict): + return 'Invalid configuration {}'.format(config) + if CONF_SERVICE not in config and CONF_SERVICE_TEMPLATE not in config: + return 'Missing key {} or {}: {}'.format( + CONF_SERVICE, + CONF_SERVICE_TEMPLATE, + config) + return None diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 73081a7da7e..b559b5563d7 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -39,6 +39,25 @@ class TestServiceHelpers(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(runs)) + def test_template_service_call(self): + """ Test service call with tempating. """ + config = { + 'service_template': '{{ \'test_domain.test_service\' }}', + 'entity_id': 'hello.world', + 'data_template': { + 'hello': '{{ \'goodbye\' }}', + }, + } + runs = [] + + decor = service.service('test_domain', 'test_service') + decor(lambda x, y: runs.append(y)) + + service.call_from_config(self.hass, config) + self.hass.pool.block_till_done() + + self.assertEqual('goodbye', runs[0].data['hello']) + def test_split_entity_string(self): service.call_from_config(self.hass, { 'service': 'test_domain.test_service', @@ -99,3 +118,21 @@ class TestServiceHelpers(unittest.TestCase): self.assertEqual(['light.ceiling', 'light.kitchen'], service.extract_entity_ids(self.hass, call)) + + def test_validate_service_call(self): + """Test is_valid_service_call method""" + self.assertNotEqual( + service.validate_service_call( + {}), + None + ) + self.assertEqual( + service.validate_service_call( + {'service': 'test_domain.test_service'}), + None + ) + self.assertEqual( + service.validate_service_call( + {'service_template': 'test_domain.{{ \'test_service\' }}'}), + None + )