diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 923d071231a..51684e5f1cd 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -231,8 +231,8 @@ EVENT_SCHEMA = vol.Schema({ SERVICE_SCHEMA = vol.All(vol.Schema({ vol.Exclusive('service', 'service name'): service, - vol.Exclusive('service_template', 'service name'): string, - vol.Exclusive('data', 'service data'): dict, - vol.Exclusive('data_template', 'service data'): {match_all: template}, - 'entity_id': entity_ids, + vol.Exclusive('service_template', 'service name'): template, + vol.Optional('data'): dict, + vol.Optional('data_template'): {match_all: template}, + vol.Optional('entity_id'): entity_ids, }), has_at_least_one_key('service', 'service_template')) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 8f366352532..8b89d856c50 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -2,9 +2,13 @@ import functools import logging +import voluptuous as vol + from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import TemplateError from homeassistant.helpers import template from homeassistant.loader import get_component +import homeassistant.helpers.config_validation as cv HASS = None @@ -28,47 +32,38 @@ def service(domain, service_name): return register_service_decorator -def call_from_config(hass, config, blocking=False): +def call_from_config(hass, config, blocking=False, variables=None): """Call a service based on a config hash.""" - 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 = domain_service.split('.', 1) - except ValueError: - _LOGGER.error('Invalid service specified: %s', domain_service) + config = cv.SERVICE_SCHEMA(config) + except vol.Invalid as ex: + _LOGGER.error("Invalid config for calling service: %s", ex) return - service_data = config.get(CONF_SERVICE_DATA) - - if service_data is None: - service_data = {} - elif isinstance(service_data, dict): - service_data = dict(service_data) + if CONF_SERVICE in config: + domain_service = config[CONF_SERVICE] else: - _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) - service_data = {} + try: + domain_service = template.render( + hass, config[CONF_SERVICE_TEMPLATE], variables) + domain_service = cv.service(domain_service) + except TemplateError as ex: + _LOGGER.error('Error rendering service name template: %s', ex) + return + except vol.Invalid as ex: + _LOGGER.error('Template rendered invalid service: %s', + domain_service) + return - 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) + domain, service_name = domain_service.split('.', 1) + service_data = dict(config.get(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 - entity_id.split(",")] - elif entity_id is not None: - service_data[ATTR_ENTITY_ID] = entity_id + if CONF_SERVICE_DATA_TEMPLATE in config: + for key, value in config[CONF_SERVICE_DATA_TEMPLATE].items(): + service_data[key] = template.render(hass, value, variables) + + if CONF_SERVICE_ENTITY_ID in config: + service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] hass.services.call(domain, service_name, service_data, blocking) @@ -98,11 +93,8 @@ def validate_service_call(config): 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 + try: + cv.SERVICE_SCHEMA(config) + return None + except vol.Invalid as ex: + return str(ex) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index be224b51ff0..59ba1781ab2 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -2,6 +2,7 @@ import unittest from unittest.mock import patch +import homeassistant.components # noqa - to prevent circular import from homeassistant import core as ha, loader from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID from homeassistant.helpers import service @@ -53,6 +54,27 @@ class TestServiceHelpers(unittest.TestCase): self.assertEqual('goodbye', runs[0].data['hello']) + def test_passing_variables_to_templates(self): + config = { + 'service_template': '{{ var_service }}', + 'entity_id': 'hello.world', + 'data_template': { + 'hello': '{{ var_data }}', + }, + } + runs = [] + + decor = service.service('test_domain', 'test_service') + decor(lambda x, y: runs.append(y)) + + service.call_from_config(self.hass, config, variables={ + 'var_service': 'test_domain.test_service', + 'var_data': 'goodbye', + }) + self.hass.pool.block_till_done() + + self.assertEqual('goodbye', runs[0].data['hello']) + def test_split_entity_string(self): """Test splitting of entity string.""" service.call_from_config(self.hass, {