From 1fb372ffdb2d17e0a357dfc3eaabc28d359ddf11 Mon Sep 17 00:00:00 2001 From: andrey-git Date: Sat, 28 Jan 2017 22:29:51 +0200 Subject: [PATCH] Apply new customize format to Zwave (#5603) --- homeassistant/components/light/zwave.py | 8 +-- homeassistant/components/zwave/__init__.py | 20 +++---- homeassistant/config.py | 40 +++---------- homeassistant/helpers/customize.py | 57 +++++++++++++----- homeassistant/helpers/entity.py | 4 +- tests/components/test_zwave.py | 68 ++++++++++++++++++++++ tests/helpers/test_customize.py | 38 +++++++++++- tests/helpers/test_entity.py | 1 + tests/test_config.py | 19 ------ 9 files changed, 171 insertions(+), 84 deletions(-) create mode 100644 tests/components/test_zwave.py diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index ab6cb3cdecd..5bab9ace4c6 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -17,6 +17,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \ color_temperature_mired_to_kelvin, color_temperature_to_rgb, \ color_rgb_to_rgbw, color_rgbw_to_rgb +from homeassistant.helpers import customize _LOGGER = logging.getLogger(__name__) @@ -54,13 +55,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - customize = hass.data['zwave_customize'] name = '{}.{}'.format(DOMAIN, zwave.object_id(value)) - node_config = customize.get(name, {}) + node_config = customize.get_overrides(hass, zwave.DOMAIN, name) refresh = node_config.get(zwave.CONF_REFRESH_VALUE) delay = node_config.get(zwave.CONF_REFRESH_DELAY) - _LOGGER.debug('customize=%s name=%s node_config=%s CONF_REFRESH_VALUE=%s' - ' CONF_REFRESH_DELAY=%s', customize, name, node_config, + _LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s' + ' CONF_REFRESH_DELAY=%s', name, node_config, refresh, delay) if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL: return diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index c4b51ca9451..5afd74c1503 100755 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -11,10 +11,10 @@ from pprint import pprint import voluptuous as vol -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, customize from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_LOCATION, ATTR_ENTITY_ID, CONF_CUSTOMIZE, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_ENTITY_ID) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_change from homeassistant.util import convert, slugify @@ -150,9 +150,9 @@ CHANGE_ASSOCIATION_SCHEMA = vol.Schema({ vol.Optional(const.ATTR_INSTANCE, default=0x00): vol.Coerce(int) }) -CUSTOMIZE_SCHEMA = vol.Schema({ - vol.Optional(CONF_POLLING_INTENSITY): - vol.All(cv.positive_int), +_ZWAVE_CUSTOMIZE_SCHEMA_ENTRY = vol.Schema({ + vol.Required(CONF_ENTITY_ID): cv.match_all, + vol.Optional(CONF_POLLING_INTENSITY): cv.positive_int, vol.Optional(CONF_IGNORED, default=DEFAULT_CONF_IGNORED): cv.boolean, vol.Optional(CONF_REFRESH_VALUE, default=DEFAULT_CONF_REFRESH_VALUE): cv.boolean, @@ -164,8 +164,9 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_AUTOHEAL, default=DEFAULT_CONF_AUTOHEAL): cv.boolean, vol.Optional(CONF_CONFIG_PATH): cv.string, - vol.Optional(CONF_CUSTOMIZE, default={}): - vol.Schema({cv.string: CUSTOMIZE_SCHEMA}), + vol.Optional(CONF_CUSTOMIZE, default=[]): + vol.All(customize.CUSTOMIZE_SCHEMA, + [_ZWAVE_CUSTOMIZE_SCHEMA_ENTRY]), vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean, vol.Optional(CONF_POLLING_INTERVAL, default=DEFAULT_POLLING_INTERVAL): cv.positive_int, @@ -268,8 +269,7 @@ def setup(hass, config): # Load configuration use_debug = config[DOMAIN].get(CONF_DEBUG) - hass.data['zwave_customize'] = config[DOMAIN].get(CONF_CUSTOMIZE) - customize = hass.data['zwave_customize'] + customize.set_customize(hass, DOMAIN, config[DOMAIN].get(CONF_CUSTOMIZE)) autoheal = config[DOMAIN].get(CONF_AUTOHEAL) # Setup options @@ -349,7 +349,7 @@ def setup(hass, config): value.genre) name = "{}.{}".format(component, object_id(value)) - node_config = customize.get(name, {}) + node_config = customize.get_overrides(hass, DOMAIN, name) if node_config.get(CONF_IGNORED): _LOGGER.info("Ignoring device %s", name) diff --git a/homeassistant/config.py b/homeassistant/config.py index 2714f066035..d8e30987e16 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, CONF_TIME_ZONE, CONF_CUSTOMIZE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, - CONF_ENTITY_ID, __version__) + __version__) from homeassistant.core import DOMAIN as CONF_CORE from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import get_component @@ -21,7 +21,7 @@ from homeassistant.util.yaml import load_yaml import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as date_util, location as loc_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from homeassistant.helpers.customize import set_customize +from homeassistant.helpers import customize _LOGGER = logging.getLogger(__name__) @@ -86,27 +86,6 @@ tts: """ -CUSTOMIZE_SCHEMA_ENTRY = vol.Schema({ - vol.Required(CONF_ENTITY_ID): vol.All( - cv.ensure_list_csv, vol.Length(min=1), [cv.string], [vol.Lower]) -}, extra=vol.ALLOW_EXTRA) - - -def _convert_old_config(inp: Any) -> List: - if not isinstance(inp, dict): - return cv.ensure_list(inp) - if CONF_ENTITY_ID in inp: - return [inp] # sigle entry - res = [] - - inp = vol.Schema({cv.match_all: dict})(inp) - for key, val in inp.items(): - val = dict(val) - val[CONF_ENTITY_ID] = key - res.append(val) - return res - - PACKAGES_CONFIG_SCHEMA = vol.Schema({ cv.slug: vol.Schema( # Package names are slugs {cv.slug: vol.Any(dict, list)}) # Only slugs for component names @@ -120,8 +99,7 @@ CORE_CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, CONF_UNIT_SYSTEM: cv.unit_system, CONF_TIME_ZONE: cv.time_zone, - vol.Optional(CONF_CUSTOMIZE, default=[]): vol.All( - _convert_old_config, [CUSTOMIZE_SCHEMA_ENTRY]), + vol.Optional(CONF_CUSTOMIZE, default=[]): customize.CUSTOMIZE_SCHEMA, vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, }) @@ -280,6 +258,7 @@ def async_process_ha_core_config(hass, config): This method is a coroutine. """ + print(CORE_CONFIG_SCHEMA) config = CORE_CONFIG_SCHEMA(config) hac = hass.config @@ -306,9 +285,9 @@ def async_process_ha_core_config(hass, config): if CONF_TIME_ZONE in config: set_time_zone(config.get(CONF_TIME_ZONE)) - customize = merge_packages_customize( + merged_customize = merge_packages_customize( config[CONF_CUSTOMIZE], config[CONF_PACKAGES]) - set_customize(hass, customize) + customize.set_customize(hass, CONF_CORE, merged_customize) if CONF_UNIT_SYSTEM in config: if config[CONF_UNIT_SYSTEM] == CONF_UNIT_SYSTEM_IMPERIAL: @@ -463,15 +442,14 @@ def merge_packages_config(config, packages): return config -def merge_packages_customize(customize, packages): +def merge_packages_customize(core_customize, packages): """Merge customize from packages.""" schema = vol.Schema({ vol.Optional(CONF_CORE): vol.Schema({ - CONF_CUSTOMIZE: vol.All( - _convert_old_config, [CUSTOMIZE_SCHEMA_ENTRY])}) + CONF_CUSTOMIZE: customize.CUSTOMIZE_SCHEMA}), }, extra=vol.ALLOW_EXTRA) - cust = list(customize) + cust = list(core_customize) for pkg in packages.values(): conf = schema(pkg) cust.extend(conf.get(CONF_CORE, {}).get(CONF_CUSTOMIZE, [])) diff --git a/homeassistant/helpers/customize.py b/homeassistant/helpers/customize.py index b03a89ff40f..e9cd7c0269a 100644 --- a/homeassistant/helpers/customize.py +++ b/homeassistant/helpers/customize.py @@ -1,25 +1,51 @@ """A helper module for customization.""" import collections -from typing import Dict, List +from typing import Any, Dict, List import fnmatch +import voluptuous as vol from homeassistant.const import CONF_ENTITY_ID from homeassistant.core import HomeAssistant, split_entity_id +import homeassistant.helpers.config_validation as cv -_OVERWRITE_KEY = 'overwrite' -_OVERWRITE_CACHE_KEY = 'overwrite_cache' +_OVERWRITE_KEY_FORMAT = '{}.overwrite' +_OVERWRITE_CACHE_KEY_FORMAT = '{}.overwrite_cache' + +_CUSTOMIZE_SCHEMA_ENTRY = vol.Schema({ + vol.Required(CONF_ENTITY_ID): vol.All( + cv.ensure_list_csv, vol.Length(min=1), [vol.Schema(str)], [vol.Lower]) +}, extra=vol.ALLOW_EXTRA) -def set_customize(hass: HomeAssistant, customize: List[Dict]) -> None: +def _convert_old_config(inp: Any) -> List: + if not isinstance(inp, dict): + return cv.ensure_list(inp) + if CONF_ENTITY_ID in inp: + return [inp] # sigle entry + res = [] + + inp = vol.Schema({cv.match_all: dict})(inp) + for key, val in inp.items(): + val = dict(val) + val[CONF_ENTITY_ID] = key + res.append(val) + return res + + +CUSTOMIZE_SCHEMA = vol.All(_convert_old_config, [_CUSTOMIZE_SCHEMA_ENTRY]) + + +def set_customize( + hass: HomeAssistant, domain: str, customize: List[Dict]) -> None: """Overwrite all current customize settings. Async friendly. """ - hass.data[_OVERWRITE_KEY] = customize - hass.data[_OVERWRITE_CACHE_KEY] = {} + hass.data[_OVERWRITE_KEY_FORMAT.format(domain)] = customize + hass.data[_OVERWRITE_CACHE_KEY_FORMAT.format(domain)] = {} -def get_overrides(hass: HomeAssistant, entity_id: str) -> Dict: +def get_overrides(hass: HomeAssistant, domain: str, entity_id: str) -> Dict: """Return a dictionary of overrides related to entity_id. Whole-domain overrides are of lowest priorities, @@ -28,10 +54,11 @@ def get_overrides(hass: HomeAssistant, entity_id: str) -> Dict: The lookups are cached. """ - if _OVERWRITE_CACHE_KEY in hass.data and \ - entity_id in hass.data[_OVERWRITE_CACHE_KEY]: - return hass.data[_OVERWRITE_CACHE_KEY][entity_id] - if _OVERWRITE_KEY not in hass.data: + cache_key = _OVERWRITE_CACHE_KEY_FORMAT.format(domain) + if cache_key in hass.data and entity_id in hass.data[cache_key]: + return hass.data[cache_key][entity_id] + overwrite_key = _OVERWRITE_KEY_FORMAT.format(domain) + if overwrite_key not in hass.data: return {} domain_result = {} # type: Dict[str, Any] glob_result = {} # type: Dict[str, Any] @@ -57,7 +84,7 @@ def get_overrides(hass: HomeAssistant, entity_id: str) -> Dict: else: target[key] = source[key] - for rule in hass.data[_OVERWRITE_KEY]: + for rule in hass.data[overwrite_key]: if CONF_ENTITY_ID in rule: entities = rule[CONF_ENTITY_ID] if domain in entities: @@ -74,7 +101,7 @@ def get_overrides(hass: HomeAssistant, entity_id: str) -> Dict: deep_update(result, clean_entry(domain_result)) deep_update(result, clean_entry(glob_result)) deep_update(result, clean_entry(exact_result)) - if _OVERWRITE_CACHE_KEY not in hass.data: - hass.data[_OVERWRITE_CACHE_KEY] = {} - hass.data[_OVERWRITE_CACHE_KEY][entity_id] = result + if cache_key not in hass.data: + hass.data[cache_key] = {} + hass.data[cache_key][entity_id] = result return result diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 6f09b9592f3..ac124b3abf3 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_ENTITY_PICTURE) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, DOMAIN as CORE_DOMAIN from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify from homeassistant.util.async import ( @@ -242,7 +242,7 @@ class Entity(object): end - start) # Overwrite properties that have been set in the config file. - attr.update(get_overrides(self.hass, self.entity_id)) + attr.update(get_overrides(self.hass, CORE_DOMAIN, self.entity_id)) # Remove hidden property if false so it won't show up. if not attr.get(ATTR_HIDDEN, True): diff --git a/tests/components/test_zwave.py b/tests/components/test_zwave.py new file mode 100644 index 00000000000..5c9be9ba22a --- /dev/null +++ b/tests/components/test_zwave.py @@ -0,0 +1,68 @@ +"""The tests for the zwave component.""" +import unittest +from unittest.mock import MagicMock, patch + +from homeassistant.bootstrap import setup_component +from homeassistant.components import zwave +from tests.common import get_test_home_assistant + + +class TestComponentZwave(unittest.TestCase): + """Test the Zwave component.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def _validate_config(self, validator, config): + libopenzwave = MagicMock() + libopenzwave.__file__ = 'test' + with patch.dict('sys.modules', { + 'libopenzwave': libopenzwave, + 'openzwave.option': MagicMock(), + 'openzwave.network': MagicMock(), + 'openzwave.group': MagicMock(), + }): + validator(setup_component(self.hass, zwave.DOMAIN, { + zwave.DOMAIN: config, + })) + + def test_empty_config(self): + """Test empty config.""" + self._validate_config(self.assertTrue, {}) + + def test_empty_customize(self): + """Test empty customize.""" + self._validate_config(self.assertTrue, {'customize': {}}) + self._validate_config(self.assertTrue, {'customize': []}) + + def test_empty_customize_content(self): + """Test empty customize.""" + self._validate_config( + self.assertTrue, {'customize': {'test.test': {}}}) + + def test_full_customize_dict(self): + """Test full customize as dict.""" + self._validate_config(self.assertTrue, {'customize': {'test.test': { + zwave.CONF_POLLING_INTENSITY: 10, + zwave.CONF_IGNORED: 1, + zwave.CONF_REFRESH_VALUE: 1, + zwave.CONF_REFRESH_DELAY: 10}}}) + + def test_full_customize_list(self): + """Test full customize as list.""" + self._validate_config(self.assertTrue, {'customize': [{ + 'entity_id': 'test.test', + zwave.CONF_POLLING_INTENSITY: 10, + zwave.CONF_IGNORED: 1, + zwave.CONF_REFRESH_VALUE: 1, + zwave.CONF_REFRESH_DELAY: 10}]}) + + def test_bad_customize(self): + """Test customize with extra keys.""" + self._validate_config( + self.assertFalse, {'customize': {'test.test': {'extra_key': 10}}}) diff --git a/tests/helpers/test_customize.py b/tests/helpers/test_customize.py index e3fd1e325b0..0fb1b6ab14c 100644 --- a/tests/helpers/test_customize.py +++ b/tests/helpers/test_customize.py @@ -1,5 +1,7 @@ """Test the customize helper.""" import homeassistant.helpers.customize as customize +from voluptuous import MultipleInvalid +import pytest class MockHass(object): @@ -17,8 +19,9 @@ class TestHelpersCustomize(object): self.hass = MockHass() def _get_overrides(self, overrides): - customize.set_customize(self.hass, overrides) - return customize.get_overrides(self.hass, self.entity_id) + test_domain = 'test.domain' + customize.set_customize(self.hass, test_domain, overrides) + return customize.get_overrides(self.hass, test_domain, self.entity_id) def test_override_single_value(self): """Test entity customization through configuration.""" @@ -75,7 +78,7 @@ class TestHelpersCustomize(object): 'key3': 'valueDomain'} def test_override_deep_dict(self): - """Test we can overwrite hidden property to True.""" + """Test we can deep-overwrite a dict.""" result = self._get_overrides( [{'entity_id': [self.entity_id], 'test': {'key1': 'value1', 'key2': 'value2'}}, @@ -85,3 +88,32 @@ class TestHelpersCustomize(object): 'key1': 'value1', 'key2': 'value22', 'key3': 'value3'} + + def test_schema_bad_schema(self): + """Test bad customize schemas.""" + for value in ( + {'test.test': 10}, + {'test.test': ['hello']}, + {'entity_id': {'a': 'b'}}, + {'entity_id': 10}, + [{'test.test': 'value'}], + ): + with pytest.raises( + MultipleInvalid, + message="{} should have raised MultipleInvalid".format( + value)): + customize.CUSTOMIZE_SCHEMA(value) + + def test_get_customize_schema_allow_extra(self): + """Test schema with ALLOW_EXTRA.""" + for value in ( + {'test.test': {'hidden': True}}, + {'test.test': {'key': ['value1', 'value2']}}, + [{'entity_id': 'id1', 'key': 'value'}], + ): + customize.CUSTOMIZE_SCHEMA(value) + + def test_get_customize_schema_csv(self): + """Test schema with comma separated entity IDs.""" + assert [{'entity_id': ['id1', 'id2', 'id3']}] == \ + customize.CUSTOMIZE_SCHEMA([{'entity_id': 'id1,ID2 , id3'}]) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 9ec016ccfcd..061c206c116 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -90,6 +90,7 @@ class TestHelpersEntity(object): """Test we can overwrite hidden property to True.""" set_customize( self.hass, + entity.CORE_DOMAIN, [{'entity_id': [self.entity.entity_id], ATTR_HIDDEN: True}]) self.entity.update_ha_state() diff --git a/tests/test_config.py b/tests/test_config.py index 1d647cb2c92..1197a0130d8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -234,25 +234,6 @@ class TestConfig(unittest.TestCase): assert state.attributes['hidden'] - def test_entity_customization_comma_separated(self): - """Test entity customization through configuration.""" - config = {CONF_LATITUDE: 50, - CONF_LONGITUDE: 50, - CONF_NAME: 'Test', - CONF_CUSTOMIZE: [ - {'entity_id': 'test.not_test,test,test.not_t*', - 'key1': 'value1'}, - {'entity_id': 'test.test,not_test,test.not_t*', - 'key2': 'value2'}, - {'entity_id': 'test.not_test,not_test,test.t*', - 'key3': 'value3'}]} - - state = self._compute_state(config) - - assert state.attributes['key1'] == 'value1' - assert state.attributes['key2'] == 'value2' - assert state.attributes['key3'] == 'value3' - @mock.patch('homeassistant.config.shutil') @mock.patch('homeassistant.config.os') def test_remove_lib_on_upgrade(self, mock_os, mock_shutil):