ps - add reload core config service (#2350)
This commit is contained in:
parent
9ce9b8debb
commit
a70f922a71
6 changed files with 156 additions and 54 deletions
|
@ -25,8 +25,8 @@ from homeassistant.const import (
|
||||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__)
|
TEMP_CELSIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__)
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
event_decorators, service, config_per_platform, extract_domain_configs,
|
||||||
from homeassistant.helpers.entity import Entity
|
entity)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_SETUP_LOCK = RLock()
|
_SETUP_LOCK = RLock()
|
||||||
|
@ -412,8 +412,7 @@ def process_ha_core_config(hass, config):
|
||||||
if CONF_TIME_ZONE in config:
|
if CONF_TIME_ZONE in config:
|
||||||
set_time_zone(config.get(CONF_TIME_ZONE))
|
set_time_zone(config.get(CONF_TIME_ZONE))
|
||||||
|
|
||||||
for entity_id, attrs in config.get(CONF_CUSTOMIZE).items():
|
entity.set_customize(config.get(CONF_CUSTOMIZE))
|
||||||
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
|
|
||||||
|
|
||||||
if CONF_TEMPERATURE_UNIT in config:
|
if CONF_TEMPERATURE_UNIT in config:
|
||||||
hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
|
hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
|
||||||
|
|
|
@ -19,6 +19,8 @@ from homeassistant.const import (
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||||
|
|
||||||
|
|
||||||
def is_on(hass, entity_id=None):
|
def is_on(hass, entity_id=None):
|
||||||
"""Load up the module to call the is_on method.
|
"""Load up the module to call the is_on method.
|
||||||
|
@ -73,6 +75,11 @@ def toggle(hass, entity_id=None, **service_data):
|
||||||
hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data)
|
hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data)
|
||||||
|
|
||||||
|
|
||||||
|
def reload_core_config(hass):
|
||||||
|
"""Reload the core config."""
|
||||||
|
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup general services related to Home Assistant."""
|
"""Setup general services related to Home Assistant."""
|
||||||
def handle_turn_service(service):
|
def handle_turn_service(service):
|
||||||
|
@ -111,4 +118,21 @@ def setup(hass, config):
|
||||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||||
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||||
|
|
||||||
|
def handle_reload_config(call):
|
||||||
|
"""Service handler for reloading core config."""
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant import config, bootstrap
|
||||||
|
|
||||||
|
try:
|
||||||
|
path = config.find_config_file(hass.config.config_dir)
|
||||||
|
conf = config.load_yaml_config_file(path)
|
||||||
|
except HomeAssistantError as err:
|
||||||
|
_LOGGER.error(err)
|
||||||
|
return
|
||||||
|
|
||||||
|
bootstrap.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {})
|
||||||
|
|
||||||
|
hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG,
|
||||||
|
handle_reload_config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -149,9 +149,9 @@ def load_yaml_config_file(config_path):
|
||||||
conf_dict = load_yaml(config_path)
|
conf_dict = load_yaml(config_path)
|
||||||
|
|
||||||
if not isinstance(conf_dict, dict):
|
if not isinstance(conf_dict, dict):
|
||||||
_LOGGER.error(
|
msg = 'The configuration file {} does not contain a dictionary'.format(
|
||||||
'The configuration file %s does not contain a dictionary',
|
|
||||||
os.path.basename(config_path))
|
os.path.basename(config_path))
|
||||||
raise HomeAssistantError()
|
_LOGGER.error(msg)
|
||||||
|
raise HomeAssistantError(msg)
|
||||||
|
|
||||||
return conf_dict
|
return conf_dict
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""An abstract class for entities."""
|
"""An abstract class for entities."""
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON,
|
ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON,
|
||||||
|
@ -10,8 +10,10 @@ from homeassistant.const import (
|
||||||
from homeassistant.exceptions import NoEntitySpecifiedError
|
from homeassistant.exceptions import NoEntitySpecifiedError
|
||||||
from homeassistant.util import ensure_unique_string, slugify
|
from homeassistant.util import ensure_unique_string, slugify
|
||||||
|
|
||||||
# Dict mapping entity_id to a boolean that overwrites the hidden property
|
# Entity attributes that we will overwrite
|
||||||
_OVERWRITE = defaultdict(dict)
|
_OVERWRITE = {}
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Pattern for validating entity IDs (format: <domain>.<entity>)
|
# Pattern for validating entity IDs (format: <domain>.<entity>)
|
||||||
ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$")
|
ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$")
|
||||||
|
@ -22,7 +24,7 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
|
||||||
name = (name or DEVICE_DEFAULT_NAME).lower()
|
name = (name or DEVICE_DEFAULT_NAME).lower()
|
||||||
if current_ids is None:
|
if current_ids is None:
|
||||||
if hass is None:
|
if hass is None:
|
||||||
raise RuntimeError("Missing required parameter currentids or hass")
|
raise ValueError("Missing required parameter currentids or hass")
|
||||||
|
|
||||||
current_ids = hass.states.entity_ids()
|
current_ids = hass.states.entity_ids()
|
||||||
|
|
||||||
|
@ -30,6 +32,13 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
|
||||||
entity_id_format.format(slugify(name)), current_ids)
|
entity_id_format.format(slugify(name)), current_ids)
|
||||||
|
|
||||||
|
|
||||||
|
def set_customize(customize):
|
||||||
|
"""Overwrite all current customize settings."""
|
||||||
|
global _OVERWRITE
|
||||||
|
|
||||||
|
_OVERWRITE = {key.lower(): val for key, val in customize.items()}
|
||||||
|
|
||||||
|
|
||||||
def split_entity_id(entity_id):
|
def split_entity_id(entity_id):
|
||||||
"""Split a state entity_id into domain, object_id."""
|
"""Split a state entity_id into domain, object_id."""
|
||||||
return entity_id.split(".", 1)
|
return entity_id.split(".", 1)
|
||||||
|
@ -207,20 +216,6 @@ class Entity(object):
|
||||||
"""Return the representation."""
|
"""Return the representation."""
|
||||||
return "<Entity {}: {}>".format(self.name, self.state)
|
return "<Entity {}: {}>".format(self.name, self.state)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def overwrite_attribute(entity_id, attrs, vals):
|
|
||||||
"""Overwrite any attribute of an entity.
|
|
||||||
|
|
||||||
This function should receive a list of attributes and a
|
|
||||||
list of values. Set attribute to None to remove any overwritten
|
|
||||||
value in place.
|
|
||||||
"""
|
|
||||||
for attr, val in zip(attrs, vals):
|
|
||||||
if val is None:
|
|
||||||
_OVERWRITE[entity_id.lower()].pop(attr, None)
|
|
||||||
else:
|
|
||||||
_OVERWRITE[entity_id.lower()][attr] = val
|
|
||||||
|
|
||||||
|
|
||||||
class ToggleEntity(Entity):
|
class ToggleEntity(Entity):
|
||||||
"""An abstract class for entities that can be turned on and off."""
|
"""An abstract class for entities that can be turned on and off."""
|
||||||
|
@ -238,11 +233,13 @@ class ToggleEntity(Entity):
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the entity on."""
|
"""Turn the entity on."""
|
||||||
pass
|
_LOGGER.warning('Method turn_on not implemented for %s',
|
||||||
|
self.entity_id)
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
pass
|
_LOGGER.warning('Method turn_off not implemented for %s',
|
||||||
|
self.entity_id)
|
||||||
|
|
||||||
def toggle(self, **kwargs):
|
def toggle(self, **kwargs):
|
||||||
"""Toggle the entity off."""
|
"""Toggle the entity off."""
|
||||||
|
|
|
@ -2,13 +2,18 @@
|
||||||
# pylint: disable=protected-access,too-many-public-methods
|
# pylint: disable=protected-access,too-many-public-methods
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
|
from homeassistant import config
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE)
|
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE)
|
||||||
import homeassistant.components as comps
|
import homeassistant.components as comps
|
||||||
|
from homeassistant.helpers import entity
|
||||||
|
|
||||||
from tests.common import get_test_home_assistant
|
from tests.common import get_test_home_assistant, mock_service
|
||||||
|
|
||||||
|
|
||||||
class TestComponentsCore(unittest.TestCase):
|
class TestComponentsCore(unittest.TestCase):
|
||||||
|
@ -31,47 +36,40 @@ class TestComponentsCore(unittest.TestCase):
|
||||||
self.assertTrue(comps.is_on(self.hass, 'light.Bowl'))
|
self.assertTrue(comps.is_on(self.hass, 'light.Bowl'))
|
||||||
self.assertFalse(comps.is_on(self.hass, 'light.Ceiling'))
|
self.assertFalse(comps.is_on(self.hass, 'light.Ceiling'))
|
||||||
self.assertTrue(comps.is_on(self.hass))
|
self.assertTrue(comps.is_on(self.hass))
|
||||||
|
self.assertFalse(comps.is_on(self.hass, 'non_existing.entity'))
|
||||||
|
|
||||||
|
def test_turn_on_without_entities(self):
|
||||||
|
"""Test turn_on method without entities."""
|
||||||
|
calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)
|
||||||
|
comps.turn_on(self.hass)
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(calls))
|
||||||
|
|
||||||
def test_turn_on(self):
|
def test_turn_on(self):
|
||||||
"""Test turn_on method."""
|
"""Test turn_on method."""
|
||||||
runs = []
|
calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)
|
||||||
self.hass.services.register(
|
|
||||||
'light', SERVICE_TURN_ON, lambda x: runs.append(1))
|
|
||||||
|
|
||||||
comps.turn_on(self.hass, 'light.Ceiling')
|
comps.turn_on(self.hass, 'light.Ceiling')
|
||||||
|
|
||||||
self.hass.pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(calls))
|
||||||
self.assertEqual(1, len(runs))
|
|
||||||
|
|
||||||
def test_turn_off(self):
|
def test_turn_off(self):
|
||||||
"""Test turn_off method."""
|
"""Test turn_off method."""
|
||||||
runs = []
|
calls = mock_service(self.hass, 'light', SERVICE_TURN_OFF)
|
||||||
self.hass.services.register(
|
|
||||||
'light', SERVICE_TURN_OFF, lambda x: runs.append(1))
|
|
||||||
|
|
||||||
comps.turn_off(self.hass, 'light.Bowl')
|
comps.turn_off(self.hass, 'light.Bowl')
|
||||||
|
|
||||||
self.hass.pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(calls))
|
||||||
self.assertEqual(1, len(runs))
|
|
||||||
|
|
||||||
def test_toggle(self):
|
def test_toggle(self):
|
||||||
"""Test toggle method."""
|
"""Test toggle method."""
|
||||||
runs = []
|
calls = mock_service(self.hass, 'light', SERVICE_TOGGLE)
|
||||||
self.hass.services.register(
|
|
||||||
'light', SERVICE_TOGGLE, lambda x: runs.append(1))
|
|
||||||
|
|
||||||
comps.toggle(self.hass, 'light.Bowl')
|
comps.toggle(self.hass, 'light.Bowl')
|
||||||
|
|
||||||
self.hass.pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(calls))
|
||||||
self.assertEqual(1, len(runs))
|
|
||||||
|
|
||||||
@patch('homeassistant.core.ServiceRegistry.call')
|
@patch('homeassistant.core.ServiceRegistry.call')
|
||||||
def test_turn_on_to_not_block_for_domains_without_service(self, mock_call):
|
def test_turn_on_to_not_block_for_domains_without_service(self, mock_call):
|
||||||
"""Test if turn_on is blocking domain with no service."""
|
"""Test if turn_on is blocking domain with no service."""
|
||||||
self.hass.services.register('light', SERVICE_TURN_ON, lambda x: x)
|
mock_service(self.hass, 'light', SERVICE_TURN_ON)
|
||||||
|
|
||||||
# We can't test if our service call results in services being called
|
# We can't test if our service call results in services being called
|
||||||
# because by mocking out the call service method, we mock out all
|
# because by mocking out the call service method, we mock out all
|
||||||
|
@ -89,3 +87,62 @@ class TestComponentsCore(unittest.TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
('sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False),
|
('sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False),
|
||||||
mock_call.call_args_list[1][0])
|
mock_call.call_args_list[1][0])
|
||||||
|
|
||||||
|
def test_reload_core_conf(self):
|
||||||
|
"""Test reload core conf service."""
|
||||||
|
ent = entity.Entity()
|
||||||
|
ent.entity_id = 'test.entity'
|
||||||
|
ent.hass = self.hass
|
||||||
|
ent.update_ha_state()
|
||||||
|
|
||||||
|
state = self.hass.states.get('test.entity')
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == 'unknown'
|
||||||
|
assert state.attributes == {}
|
||||||
|
|
||||||
|
with TemporaryDirectory() as conf_dir:
|
||||||
|
self.hass.config.config_dir = conf_dir
|
||||||
|
conf_yaml = self.hass.config.path(config.YAML_CONFIG_FILE)
|
||||||
|
|
||||||
|
with open(conf_yaml, 'a') as fp:
|
||||||
|
fp.write(yaml.dump({
|
||||||
|
ha.DOMAIN: {
|
||||||
|
'latitude': 10,
|
||||||
|
'longitude': 20,
|
||||||
|
'customize': {
|
||||||
|
'test.Entity': {
|
||||||
|
'hello': 'world'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
comps.reload_core_config(self.hass)
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
|
assert 10 == self.hass.config.latitude
|
||||||
|
assert 20 == self.hass.config.longitude
|
||||||
|
|
||||||
|
ent.update_ha_state()
|
||||||
|
|
||||||
|
state = self.hass.states.get('test.entity')
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == 'unknown'
|
||||||
|
assert state.attributes.get('hello') == 'world'
|
||||||
|
|
||||||
|
@patch('homeassistant.components._LOGGER.error')
|
||||||
|
@patch('homeassistant.bootstrap.process_ha_core_config')
|
||||||
|
def test_reload_core_with_wrong_conf(self, mock_process, mock_error):
|
||||||
|
"""Test reload core conf service."""
|
||||||
|
with TemporaryDirectory() as conf_dir:
|
||||||
|
self.hass.config.config_dir = conf_dir
|
||||||
|
conf_yaml = self.hass.config.path(config.YAML_CONFIG_FILE)
|
||||||
|
|
||||||
|
with open(conf_yaml, 'a') as fp:
|
||||||
|
fp.write(yaml.dump(['invalid', 'config']))
|
||||||
|
|
||||||
|
comps.reload_core_config(self.hass)
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
|
assert mock_error.called
|
||||||
|
assert mock_process.called is False
|
||||||
|
|
|
@ -21,8 +21,7 @@ class TestHelpersEntity(unittest.TestCase):
|
||||||
def tearDown(self): # pylint: disable=invalid-name
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
"""Stop everything that was started."""
|
"""Stop everything that was started."""
|
||||||
self.hass.stop()
|
self.hass.stop()
|
||||||
entity.Entity.overwrite_attribute(self.entity.entity_id,
|
entity.set_customize({})
|
||||||
[ATTR_HIDDEN], [None])
|
|
||||||
|
|
||||||
def test_default_hidden_not_in_attributes(self):
|
def test_default_hidden_not_in_attributes(self):
|
||||||
"""Test that the default hidden property is set to False."""
|
"""Test that the default hidden property is set to False."""
|
||||||
|
@ -32,8 +31,7 @@ class TestHelpersEntity(unittest.TestCase):
|
||||||
|
|
||||||
def test_overwriting_hidden_property_to_true(self):
|
def test_overwriting_hidden_property_to_true(self):
|
||||||
"""Test we can overwrite hidden property to True."""
|
"""Test we can overwrite hidden property to True."""
|
||||||
entity.Entity.overwrite_attribute(self.entity.entity_id,
|
entity.set_customize({self.entity.entity_id: {ATTR_HIDDEN: True}})
|
||||||
[ATTR_HIDDEN], [True])
|
|
||||||
self.entity.update_ha_state()
|
self.entity.update_ha_state()
|
||||||
|
|
||||||
state = self.hass.states.get(self.entity.entity_id)
|
state = self.hass.states.get(self.entity.entity_id)
|
||||||
|
@ -43,3 +41,30 @@ class TestHelpersEntity(unittest.TestCase):
|
||||||
"""Test split_entity_id."""
|
"""Test split_entity_id."""
|
||||||
self.assertEqual(['domain', 'object_id'],
|
self.assertEqual(['domain', 'object_id'],
|
||||||
entity.split_entity_id('domain.object_id'))
|
entity.split_entity_id('domain.object_id'))
|
||||||
|
|
||||||
|
def test_generate_entity_id_requires_hass_or_ids(self):
|
||||||
|
"""Ensure we require at least hass or current ids."""
|
||||||
|
fmt = 'test.{}'
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
entity.generate_entity_id(fmt, 'hello world')
|
||||||
|
|
||||||
|
def test_generate_entity_id_given_hass(self):
|
||||||
|
"""Test generating an entity id given hass object."""
|
||||||
|
fmt = 'test.{}'
|
||||||
|
self.assertEqual(
|
||||||
|
'test.overwrite_hidden_true_2',
|
||||||
|
entity.generate_entity_id(fmt, 'overwrite hidden true',
|
||||||
|
hass=self.hass))
|
||||||
|
|
||||||
|
def test_generate_entity_id_given_keys(self):
|
||||||
|
"""Test generating an entity id given current ids."""
|
||||||
|
fmt = 'test.{}'
|
||||||
|
self.assertEqual(
|
||||||
|
'test.overwrite_hidden_true_2',
|
||||||
|
entity.generate_entity_id(
|
||||||
|
fmt, 'overwrite hidden true',
|
||||||
|
current_ids=['test.overwrite_hidden_true']))
|
||||||
|
self.assertEqual(
|
||||||
|
'test.overwrite_hidden_true',
|
||||||
|
entity.generate_entity_id(fmt, 'overwrite hidden true',
|
||||||
|
current_ids=['test.another_entity']))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue