Use a template for the Universal media player state (#10395)

* Implementation of `state_template` for the Universal media_player

* add tracking to entities in state template

* use normal config_validation

* fix tests, use defaults in platform schema, remove extra keys

* and test the new option `state_template`

* lint fixes

* no need to check attributes against None

* use `async_added_to_hass` and call `async_track_state_change` from `hass.helpers`
This commit is contained in:
Eugenio Panadero 2017-11-14 11:41:19 +01:00 committed by Pascal Vizeli
parent dc6e50c39d
commit e947e6a143
3 changed files with 155 additions and 177 deletions

View file

@ -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:

View file

@ -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'

View file

@ -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)
setup_ok = True
try:
run_coroutine_threadsafe(
universal.async_setup_platform(self.hass, bad_config, add_devices),
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')