diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 9647f04f5c3..a7173e35a48 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -9,28 +9,30 @@ import logging # pylint: disable=import-error from copy import copy +import voluptuous as vol + from homeassistant.core import callback from homeassistant.components.media_player import ( - ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, - ATTR_MEDIA_ARTIST, ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE, - ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION, - ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, - ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_INPUT_SOURCE_LIST, - ATTR_MEDIA_POSITION, ATTR_MEDIA_SHUFFLE, - ATTR_MEDIA_POSITION_UPDATED_AT, DOMAIN, SERVICE_PLAY_MEDIA, + ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, + ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST, + ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE, ATTR_MEDIA_PLAYLIST, + ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_SEASON, + ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_SHUFFLE, + ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, DOMAIN, MediaPlayerDevice, PLATFORM_SCHEMA, + SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, + SUPPORT_CLEAR_PLAYLIST, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, - SUPPORT_SHUFFLE_SET, ATTR_INPUT_SOURCE, SERVICE_SELECT_SOURCE, - SERVICE_CLEAR_PLAYLIST, MediaPlayerDevice) + SUPPORT_VOLUME_STEP) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK, - SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, - SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, - SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, - SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, SERVICE_SHUFFLE_SET, STATE_IDLE, - STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP, ATTR_SUPPORTED_FEATURES) -from homeassistant.helpers.event import async_track_state_change + ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, CONF_NAME, + CONF_STATE_TEMPLATE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_call_from_config ATTR_ACTIVE_CHILD = 'active_child' @@ -48,113 +50,75 @@ OFF_STATES = [STATE_IDLE, STATE_OFF] REQUIREMENTS = [] _LOGGER = logging.getLogger(__name__) +ATTRS_SCHEMA = vol.Schema({cv.slug: cv.string}) +CMD_SCHEMA = vol.Schema({cv.slug: cv.SERVICE_SCHEMA}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_CHILDREN, default=[]): cv.entity_ids, + vol.Optional(CONF_COMMANDS, default={}): CMD_SCHEMA, + vol.Optional(CONF_ATTRS, default={}): + vol.Or(cv.ensure_list(ATTRS_SCHEMA), ATTRS_SCHEMA), + vol.Optional(CONF_STATE_TEMPLATE): cv.template +}, extra=vol.REMOVE_EXTRA) + @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the universal media players.""" - if not validate_config(config): - return - player = UniversalMediaPlayer( hass, - config[CONF_NAME], - config[CONF_CHILDREN], - config[CONF_COMMANDS], - config[CONF_ATTRS] + config.get(CONF_NAME), + config.get(CONF_CHILDREN), + config.get(CONF_COMMANDS), + config.get(CONF_ATTRS), + config.get(CONF_STATE_TEMPLATE) ) async_add_devices([player]) -def validate_config(config): - """Validate universal media player configuration.""" - del config[CONF_PLATFORM] - - # Validate name - if CONF_NAME not in config: - _LOGGER.error("Universal Media Player configuration requires name") - return False - - validate_children(config) - validate_commands(config) - validate_attributes(config) - - del_keys = [] - for key in config: - if key not in [CONF_NAME, CONF_CHILDREN, CONF_COMMANDS, CONF_ATTRS]: - _LOGGER.warning( - "Universal Media Player (%s) unrecognized parameter %s", - config[CONF_NAME], key) - del_keys.append(key) - for key in del_keys: - del config[key] - - return True - - -def validate_children(config): - """Validate children.""" - if CONF_CHILDREN not in config: - _LOGGER.info( - "No children under Universal Media Player (%s)", config[CONF_NAME]) - config[CONF_CHILDREN] = [] - elif not isinstance(config[CONF_CHILDREN], list): - _LOGGER.warning( - "Universal Media Player (%s) children not list in config. " - "They will be ignored", config[CONF_NAME]) - config[CONF_CHILDREN] = [] - - -def validate_commands(config): - """Validate commands.""" - if CONF_COMMANDS not in config: - config[CONF_COMMANDS] = {} - elif not isinstance(config[CONF_COMMANDS], dict): - _LOGGER.warning( - "Universal Media Player (%s) specified commands not dict in " - "config. They will be ignored", config[CONF_NAME]) - config[CONF_COMMANDS] = {} - - -def validate_attributes(config): - """Validate attributes.""" - if CONF_ATTRS not in config: - config[CONF_ATTRS] = {} - elif not isinstance(config[CONF_ATTRS], dict): - _LOGGER.warning( - "Universal Media Player (%s) specified attributes " - "not dict in config. They will be ignored", config[CONF_NAME]) - config[CONF_ATTRS] = {} - - for key, val in config[CONF_ATTRS].items(): - attr = val.split('|', 1) - if len(attr) == 1: - attr.append(None) - config[CONF_ATTRS][key] = attr - - class UniversalMediaPlayer(MediaPlayerDevice): """Representation of an universal media player.""" - def __init__(self, hass, name, children, commands, attributes): + def __init__(self, hass, name, children, + commands, attributes, state_template=None): """Initialize the Universal media device.""" self.hass = hass self._name = name self._children = children self._cmds = commands - self._attrs = attributes + self._attrs = {} + for key, val in attributes.items(): + attr = val.split('|', 1) + if len(attr) == 1: + attr.append(None) + self._attrs[key] = attr self._child_state = None + self._state_template = state_template + if state_template is not None: + self._state_template.hass = hass + @asyncio.coroutine + def async_added_to_hass(self): + """Subscribe to children and template state changes. + + This method must be run in the event loop and returns a coroutine. + """ @callback def async_on_dependency_update(*_): """Update ha state when dependencies update.""" self.async_schedule_update_ha_state(True) - depend = copy(children) - for entity in attributes.values(): + depend = copy(self._children) + for entity in self._attrs.values(): depend.append(entity[0]) + if self._state_template is not None: + for entity in self._state_template.extract_entities(): + depend.append(entity) - async_track_state_change(hass, depend, async_on_dependency_update) + self.hass.helpers.event.async_track_state_change( + list(set(depend)), async_on_dependency_update) def _entity_lkp(self, entity_id, state_attr=None): """Look up an entity state.""" @@ -211,6 +175,8 @@ class UniversalMediaPlayer(MediaPlayerDevice): @property def master_state(self): """Return the master state for entity or None.""" + if self._state_template is not None: + return self._state_template.async_render() if CONF_STATE in self._attrs: master_state = self._entity_lkp( self._attrs[CONF_STATE][0], self._attrs[CONF_STATE][1]) @@ -232,8 +198,8 @@ class UniversalMediaPlayer(MediaPlayerDevice): else master state or off """ master_state = self.master_state # avoid multiple lookups - if master_state == STATE_OFF: - return STATE_OFF + if (master_state == STATE_OFF) or (self._state_template is not None): + return master_state active_child = self._child_state if active_child: diff --git a/homeassistant/const.py b/homeassistant/const.py index 90aa2c52483..d08308de820 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -126,6 +126,7 @@ CONF_SHOW_ON_MAP = 'show_on_map' CONF_SLAVE = 'slave' CONF_SSL = 'ssl' CONF_STATE = 'state' +CONF_STATE_TEMPLATE = 'state_template' CONF_STRUCTURE = 'structure' CONF_SWITCHES = 'switches' CONF_TEMPERATURE_UNIT = 'temperature_unit' diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index 01281d189b4..ffd4008f385 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -2,6 +2,8 @@ from copy import copy import unittest +from voluptuous.error import MultipleInvalid + from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED) import homeassistant.components.switch as switch @@ -14,6 +16,13 @@ from homeassistant.util.async import run_coroutine_threadsafe from tests.common import mock_service, get_test_home_assistant +def validate_config(config): + """Use the platform schema to validate configuration.""" + validated_config = universal.PLATFORM_SCHEMA(config) + validated_config.pop('platform') + return validated_config + + class MockMediaPlayer(media_player.MediaPlayerDevice): """Mock media player for testing.""" @@ -116,9 +125,9 @@ class MockMediaPlayer(media_player.MediaPlayerDevice): """Mock turn_off function.""" self._state = STATE_OFF - def mute_volume(self): + def mute_volume(self, mute): """Mock mute function.""" - self._is_volume_muted = ~self._is_volume_muted + self._is_volume_muted = mute def set_volume_level(self, volume): """Mock set volume level.""" @@ -210,10 +219,8 @@ class TestMediaPlayer(unittest.TestCase): config_start['commands'] = {} config_start['attributes'] = {} - response = universal.validate_config(self.config_children_only) - - self.assertTrue(response) - self.assertEqual(config_start, self.config_children_only) + config = validate_config(self.config_children_only) + self.assertEqual(config_start, config) def test_config_children_and_attr(self): """Check config with children and attributes.""" @@ -221,15 +228,16 @@ class TestMediaPlayer(unittest.TestCase): del config_start['platform'] config_start['commands'] = {} - response = universal.validate_config(self.config_children_and_attr) - - self.assertTrue(response) - self.assertEqual(config_start, self.config_children_and_attr) + config = validate_config(self.config_children_and_attr) + self.assertEqual(config_start, config) def test_config_no_name(self): """Check config with no Name entry.""" - response = universal.validate_config({'platform': 'universal'}) - + response = True + try: + validate_config({'platform': 'universal'}) + except MultipleInvalid: + response = False self.assertFalse(response) def test_config_bad_children(self): @@ -238,36 +246,31 @@ class TestMediaPlayer(unittest.TestCase): config_bad_children = {'name': 'test', 'children': {}, 'platform': 'universal'} - response = universal.validate_config(config_no_children) - self.assertTrue(response) + config_no_children = validate_config(config_no_children) self.assertEqual([], config_no_children['children']) - response = universal.validate_config(config_bad_children) - self.assertTrue(response) + config_bad_children = validate_config(config_bad_children) self.assertEqual([], config_bad_children['children']) def test_config_bad_commands(self): """Check config with bad commands entry.""" - config = {'name': 'test', 'commands': [], 'platform': 'universal'} + config = {'name': 'test', 'platform': 'universal'} - response = universal.validate_config(config) - self.assertTrue(response) + config = validate_config(config) self.assertEqual({}, config['commands']) def test_config_bad_attributes(self): """Check config with bad attributes.""" - config = {'name': 'test', 'attributes': [], 'platform': 'universal'} + config = {'name': 'test', 'platform': 'universal'} - response = universal.validate_config(config) - self.assertTrue(response) + config = validate_config(config) self.assertEqual({}, config['attributes']) def test_config_bad_key(self): """Check config with bad key.""" config = {'name': 'test', 'asdf': 5, 'platform': 'universal'} - response = universal.validate_config(config) - self.assertTrue(response) + config = validate_config(config) self.assertFalse('asdf' in config) def test_platform_setup(self): @@ -281,21 +284,27 @@ class TestMediaPlayer(unittest.TestCase): for dev in new_entities: entities.append(dev) - run_coroutine_threadsafe( - universal.async_setup_platform(self.hass, bad_config, add_devices), - self.hass.loop).result() + setup_ok = True + try: + run_coroutine_threadsafe( + universal.async_setup_platform( + self.hass, validate_config(bad_config), add_devices), + self.hass.loop).result() + except MultipleInvalid: + setup_ok = False + self.assertFalse(setup_ok) self.assertEqual(0, len(entities)) run_coroutine_threadsafe( - universal.async_setup_platform(self.hass, config, add_devices), + universal.async_setup_platform( + self.hass, validate_config(config), add_devices), self.hass.loop).result() self.assertEqual(1, len(entities)) self.assertEqual('test', entities[0].name) def test_master_state(self): """Test master state property.""" - config = self.config_children_only - universal.validate_config(config) + config = validate_config(self.config_children_only) ump = universal.UniversalMediaPlayer(self.hass, **config) @@ -303,8 +312,7 @@ class TestMediaPlayer(unittest.TestCase): def test_master_state_with_attrs(self): """Test master state property.""" - config = self.config_children_and_attr - universal.validate_config(config) + config = validate_config(self.config_children_and_attr) ump = universal.UniversalMediaPlayer(self.hass, **config) @@ -312,11 +320,26 @@ class TestMediaPlayer(unittest.TestCase): self.hass.states.set(self.mock_state_switch_id, STATE_ON) self.assertEqual(STATE_ON, ump.master_state) + def test_master_state_with_template(self): + """Test the state_template option.""" + config = copy(self.config_children_and_attr) + self.hass.states.set('input_boolean.test', STATE_OFF) + templ = '{% if states.input_boolean.test.state == "off" %}on' \ + '{% else %}{{ states.media_player.mock1.state }}{% endif %}' + config['state_template'] = templ + config = validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(STATE_ON, ump.master_state) + self.hass.states.set('input_boolean.test', STATE_ON) + self.assertEqual(STATE_OFF, ump.master_state) + def test_master_state_with_bad_attrs(self): """Test master state property.""" - config = self.config_children_and_attr + config = copy(self.config_children_and_attr) config['attributes']['state'] = 'bad.entity_id' - universal.validate_config(config) + config = validate_config(config) ump = universal.UniversalMediaPlayer(self.hass, **config) @@ -324,8 +347,7 @@ class TestMediaPlayer(unittest.TestCase): def test_active_child_state(self): """Test active child state property.""" - config = self.config_children_only - universal.validate_config(config) + config = validate_config(self.config_children_only) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) @@ -356,8 +378,7 @@ class TestMediaPlayer(unittest.TestCase): def test_name(self): """Test name property.""" - config = self.config_children_only - universal.validate_config(config) + config = validate_config(self.config_children_only) ump = universal.UniversalMediaPlayer(self.hass, **config) @@ -365,8 +386,7 @@ class TestMediaPlayer(unittest.TestCase): def test_polling(self): """Test should_poll property.""" - config = self.config_children_only - universal.validate_config(config) + config = validate_config(self.config_children_only) ump = universal.UniversalMediaPlayer(self.hass, **config) @@ -374,8 +394,7 @@ class TestMediaPlayer(unittest.TestCase): def test_state_children_only(self): """Test media player state with only children.""" - config = self.config_children_only - universal.validate_config(config) + config = validate_config(self.config_children_only) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) @@ -391,8 +410,7 @@ class TestMediaPlayer(unittest.TestCase): def test_state_with_children_and_attrs(self): """Test media player with children and master state.""" - config = self.config_children_and_attr - universal.validate_config(config) + config = validate_config(self.config_children_and_attr) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) @@ -416,8 +434,7 @@ class TestMediaPlayer(unittest.TestCase): def test_volume_level(self): """Test volume level property.""" - config = self.config_children_only - universal.validate_config(config) + config = validate_config(self.config_children_only) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) @@ -439,9 +456,8 @@ class TestMediaPlayer(unittest.TestCase): def test_media_image_url(self): """Test media_image_url property.""" - TEST_URL = "test_url" - config = self.config_children_only - universal.validate_config(config) + test_url = "test_url" + config = validate_config(self.config_children_only) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) @@ -450,7 +466,7 @@ class TestMediaPlayer(unittest.TestCase): self.assertEqual(None, ump.media_image_url) self.mock_mp_1._state = STATE_PLAYING - self.mock_mp_1._media_image_url = TEST_URL + self.mock_mp_1._media_image_url = test_url self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() @@ -460,8 +476,7 @@ class TestMediaPlayer(unittest.TestCase): def test_is_volume_muted_children_only(self): """Test is volume muted property w/ children only.""" - config = self.config_children_only - universal.validate_config(config) + config = validate_config(self.config_children_only) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) @@ -483,8 +498,7 @@ class TestMediaPlayer(unittest.TestCase): def test_source_list_children_and_attr(self): """Test source list property w/ children and attrs.""" - config = self.config_children_and_attr - universal.validate_config(config) + config = validate_config(self.config_children_and_attr) ump = universal.UniversalMediaPlayer(self.hass, **config) @@ -495,8 +509,7 @@ class TestMediaPlayer(unittest.TestCase): def test_source_children_and_attr(self): """Test source property w/ children and attrs.""" - config = self.config_children_and_attr - universal.validate_config(config) + config = validate_config(self.config_children_and_attr) ump = universal.UniversalMediaPlayer(self.hass, **config) @@ -507,8 +520,7 @@ class TestMediaPlayer(unittest.TestCase): def test_volume_level_children_and_attr(self): """Test volume level property w/ children and attrs.""" - config = self.config_children_and_attr - universal.validate_config(config) + config = validate_config(self.config_children_and_attr) ump = universal.UniversalMediaPlayer(self.hass, **config) @@ -519,8 +531,7 @@ class TestMediaPlayer(unittest.TestCase): def test_is_volume_muted_children_and_attr(self): """Test is volume muted property w/ children and attrs.""" - config = self.config_children_and_attr - universal.validate_config(config) + config = validate_config(self.config_children_and_attr) ump = universal.UniversalMediaPlayer(self.hass, **config) @@ -531,8 +542,7 @@ class TestMediaPlayer(unittest.TestCase): def test_supported_features_children_only(self): """Test supported media commands with only children.""" - config = self.config_children_only - universal.validate_config(config) + config = validate_config(self.config_children_only) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) @@ -549,16 +559,19 @@ class TestMediaPlayer(unittest.TestCase): def test_supported_features_children_and_cmds(self): """Test supported media commands with children and attrs.""" - config = self.config_children_and_attr - universal.validate_config(config) - config['commands']['turn_on'] = 'test' - config['commands']['turn_off'] = 'test' - config['commands']['volume_up'] = 'test' - config['commands']['volume_down'] = 'test' - config['commands']['volume_mute'] = 'test' - config['commands']['volume_set'] = 'test' - config['commands']['select_source'] = 'test' - config['commands']['shuffle_set'] = 'test' + config = copy(self.config_children_and_attr) + excmd = {'service': 'media_player.test', 'data': {'entity_id': 'test'}} + config['commands'] = { + 'turn_on': excmd, + 'turn_off': excmd, + 'volume_up': excmd, + 'volume_down': excmd, + 'volume_mute': excmd, + 'volume_set': excmd, + 'select_source': excmd, + 'shuffle_set': excmd + } + config = validate_config(config) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) @@ -577,8 +590,7 @@ class TestMediaPlayer(unittest.TestCase): def test_service_call_no_active_child(self): """Test a service call to children with no active child.""" - config = self.config_children_only - universal.validate_config(config) + config = validate_config(self.config_children_and_attr) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) @@ -599,8 +611,7 @@ class TestMediaPlayer(unittest.TestCase): def test_service_call_to_child(self): """Test service calls that should be routed to a child.""" - config = self.config_children_only - universal.validate_config(config) + config = validate_config(self.config_children_only) ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) @@ -699,10 +710,10 @@ class TestMediaPlayer(unittest.TestCase): def test_service_call_to_command(self): """Test service call to command.""" - config = self.config_children_only + config = copy(self.config_children_only) config['commands'] = {'turn_off': { 'service': 'test.turn_off', 'data': {}}} - universal.validate_config(config) + config = validate_config(config) service = mock_service(self.hass, 'test', 'turn_off')