diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 99382bebe74..754d4f4f5aa 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -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] diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index f2696bbbd1a..d625f9cd3cd 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -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 diff --git a/homeassistant/config.py b/homeassistant/config.py index b89d358045d..e8981e520c8 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -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 diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 423a276f11a..ee1b786dce3 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -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: .) 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 "".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.""" diff --git a/tests/components/test_init.py b/tests/components/test_init.py index ff663a95a53..68b0ca3be35 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -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 diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index a317702b29f..a465c2f2c74 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -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']))