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__)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
event_decorators, service, config_per_platform, extract_domain_configs,
|
||||
entity)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SETUP_LOCK = RLock()
|
||||
|
@ -412,8 +412,7 @@ def process_ha_core_config(hass, config):
|
|||
if CONF_TIME_ZONE in config:
|
||||
set_time_zone(config.get(CONF_TIME_ZONE))
|
||||
|
||||
for entity_id, attrs in config.get(CONF_CUSTOMIZE).items():
|
||||
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
|
||||
entity.set_customize(config.get(CONF_CUSTOMIZE))
|
||||
|
||||
if CONF_TEMPERATURE_UNIT in config:
|
||||
hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
|
||||
|
|
|
@ -19,6 +19,8 @@ from homeassistant.const import (
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
"""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)
|
||||
|
||||
|
||||
def reload_core_config(hass):
|
||||
"""Reload the core config."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup general services related to Home Assistant."""
|
||||
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_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
|
||||
|
|
|
@ -149,9 +149,9 @@ def load_yaml_config_file(config_path):
|
|||
conf_dict = load_yaml(config_path)
|
||||
|
||||
if not isinstance(conf_dict, dict):
|
||||
_LOGGER.error(
|
||||
'The configuration file %s does not contain a dictionary',
|
||||
msg = 'The configuration file {} does not contain a dictionary'.format(
|
||||
os.path.basename(config_path))
|
||||
raise HomeAssistantError()
|
||||
_LOGGER.error(msg)
|
||||
raise HomeAssistantError(msg)
|
||||
|
||||
return conf_dict
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""An abstract class for entities."""
|
||||
import logging
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from homeassistant.const import (
|
||||
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.util import ensure_unique_string, slugify
|
||||
|
||||
# Dict mapping entity_id to a boolean that overwrites the hidden property
|
||||
_OVERWRITE = defaultdict(dict)
|
||||
# Entity attributes that we will overwrite
|
||||
_OVERWRITE = {}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Pattern for validating entity IDs (format: <domain>.<entity>)
|
||||
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()
|
||||
if current_ids 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()
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
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):
|
||||
"""Split a state entity_id into domain, object_id."""
|
||||
return entity_id.split(".", 1)
|
||||
|
@ -207,20 +216,6 @@ class Entity(object):
|
|||
"""Return the representation."""
|
||||
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):
|
||||
"""An abstract class for entities that can be turned on and off."""
|
||||
|
@ -238,11 +233,13 @@ class ToggleEntity(Entity):
|
|||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
pass
|
||||
_LOGGER.warning('Method turn_on not implemented for %s',
|
||||
self.entity_id)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
pass
|
||||
_LOGGER.warning('Method turn_off not implemented for %s',
|
||||
self.entity_id)
|
||||
|
||||
def toggle(self, **kwargs):
|
||||
"""Toggle the entity off."""
|
||||
|
|
|
@ -2,13 +2,18 @@
|
|||
# pylint: disable=protected-access,too-many-public-methods
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import yaml
|
||||
|
||||
import homeassistant.core as ha
|
||||
from homeassistant import config
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE)
|
||||
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):
|
||||
|
@ -31,47 +36,40 @@ class TestComponentsCore(unittest.TestCase):
|
|||
self.assertTrue(comps.is_on(self.hass, 'light.Bowl'))
|
||||
self.assertFalse(comps.is_on(self.hass, 'light.Ceiling'))
|
||||
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):
|
||||
"""Test turn_on method."""
|
||||
runs = []
|
||||
self.hass.services.register(
|
||||
'light', SERVICE_TURN_ON, lambda x: runs.append(1))
|
||||
|
||||
calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)
|
||||
comps.turn_on(self.hass, 'light.Ceiling')
|
||||
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(1, len(runs))
|
||||
self.assertEqual(1, len(calls))
|
||||
|
||||
def test_turn_off(self):
|
||||
"""Test turn_off method."""
|
||||
runs = []
|
||||
self.hass.services.register(
|
||||
'light', SERVICE_TURN_OFF, lambda x: runs.append(1))
|
||||
|
||||
calls = mock_service(self.hass, 'light', SERVICE_TURN_OFF)
|
||||
comps.turn_off(self.hass, 'light.Bowl')
|
||||
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(1, len(runs))
|
||||
self.assertEqual(1, len(calls))
|
||||
|
||||
def test_toggle(self):
|
||||
"""Test toggle method."""
|
||||
runs = []
|
||||
self.hass.services.register(
|
||||
'light', SERVICE_TOGGLE, lambda x: runs.append(1))
|
||||
|
||||
calls = mock_service(self.hass, 'light', SERVICE_TOGGLE)
|
||||
comps.toggle(self.hass, 'light.Bowl')
|
||||
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
self.assertEqual(1, len(runs))
|
||||
self.assertEqual(1, len(calls))
|
||||
|
||||
@patch('homeassistant.core.ServiceRegistry.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."""
|
||||
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
|
||||
# because by mocking out the call service method, we mock out all
|
||||
|
@ -89,3 +87,62 @@ class TestComponentsCore(unittest.TestCase):
|
|||
self.assertEqual(
|
||||
('sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False),
|
||||
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
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
entity.Entity.overwrite_attribute(self.entity.entity_id,
|
||||
[ATTR_HIDDEN], [None])
|
||||
entity.set_customize({})
|
||||
|
||||
def test_default_hidden_not_in_attributes(self):
|
||||
"""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):
|
||||
"""Test we can overwrite hidden property to True."""
|
||||
entity.Entity.overwrite_attribute(self.entity.entity_id,
|
||||
[ATTR_HIDDEN], [True])
|
||||
entity.set_customize({self.entity.entity_id: {ATTR_HIDDEN: True}})
|
||||
self.entity.update_ha_state()
|
||||
|
||||
state = self.hass.states.get(self.entity.entity_id)
|
||||
|
@ -43,3 +41,30 @@ class TestHelpersEntity(unittest.TestCase):
|
|||
"""Test split_entity_id."""
|
||||
self.assertEqual(['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