From 9631179126615549d914a7390567275cbba732aa Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 23 Sep 2016 09:12:11 +0200 Subject: [PATCH] Use voluptuous for input_slider, input_boolean, input_select (#3256) * Use voluptuous for input slider * floats * _setup_component * Imperative mood * CONFIG_SCHEMA * None returns empty ensure_list * allow_extra * bool * restore ensure_list behaviour --- homeassistant/bootstrap.py | 8 +-- homeassistant/components/input_boolean.py | 24 +++---- homeassistant/components/input_select.py | 52 +++++++-------- homeassistant/components/input_slider.py | 52 ++++++++------- homeassistant/helpers/__init__.py | 6 +- homeassistant/helpers/config_validation.py | 6 +- tests/components/test_input_boolean.py | 76 +++++++++++----------- tests/components/test_input_select.py | 74 +++++++++------------ tests/components/test_input_slider.py | 53 +++++++-------- 9 files changed, 166 insertions(+), 185 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 083fe639fea..d1ac26250aa 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -145,7 +145,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, if hasattr(component, 'CONFIG_SCHEMA'): try: config = component.CONFIG_SCHEMA(config) - except vol.MultipleInvalid as ex: + except vol.Invalid as ex: log_exception(ex, domain, config) return None @@ -155,8 +155,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, # Validate component specific platform schema try: p_validated = component.PLATFORM_SCHEMA(p_config) - except vol.MultipleInvalid as ex: - log_exception(ex, domain, p_config) + except vol.Invalid as ex: + log_exception(ex, domain, config) return None # Not all platform components follow same pattern for platforms @@ -176,7 +176,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, if hasattr(platform, 'PLATFORM_SCHEMA'): try: p_validated = platform.PLATFORM_SCHEMA(p_validated) - except vol.MultipleInvalid as ex: + except vol.Invalid as ex: log_exception(ex, '{}.{}'.format(domain, p_name), p_validated) return None diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 5d02f71391f..f3635932aaa 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -9,12 +9,11 @@ import logging import voluptuous as vol from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_TOGGLE, - STATE_ON) + ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_TOGGLE, STATE_ON) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.util import slugify DOMAIN = 'input_boolean' @@ -22,14 +21,19 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' _LOGGER = logging.getLogger(__name__) -CONF_NAME = "name" -CONF_INITIAL = "initial" -CONF_ICON = "icon" +CONF_INITIAL = 'initial' SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) +CONFIG_SCHEMA = vol.Schema({ + cv.slug: { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, + }}, extra=vol.ALLOW_EXTRA) + def is_on(hass, entity_id): """Test if input_boolean is True.""" @@ -53,19 +57,11 @@ def toggle(hass, entity_id): def setup(hass, config): """Set up input boolean.""" - if not isinstance(config.get(DOMAIN), dict): - _LOGGER.error('Expected %s config to be a dictionary', DOMAIN) - return False - component = EntityComponent(_LOGGER, DOMAIN, hass) entities = [] for object_id, cfg in config[DOMAIN].items(): - if object_id != slugify(object_id): - _LOGGER.warning("Found invalid key for boolean input: %s. " - "Use %s instead", object_id, slugify(object_id)) - continue if not cfg: cfg = {} diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index 15b4c398287..33c0757f266 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -8,19 +8,17 @@ import logging import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.util import slugify + DOMAIN = 'input_select' ENTITY_ID_FORMAT = DOMAIN + '.{}' _LOGGER = logging.getLogger(__name__) -CONF_NAME = 'name' CONF_INITIAL = 'initial' -CONF_ICON = 'icon' CONF_OPTIONS = 'options' ATTR_OPTION = 'option' @@ -34,6 +32,26 @@ SERVICE_SELECT_OPTION_SCHEMA = vol.Schema({ }) +def _cv_input_select(cfg): + """Config validation helper for input select (Voluptuous).""" + options = cfg[CONF_OPTIONS] + state = cfg.get(CONF_INITIAL, options[0]) + if state not in options: + raise vol.Invalid('initial state "{}" is not part of the options: {}' + .format(state, ','.join(options))) + return cfg + + +CONFIG_SCHEMA = vol.Schema({DOMAIN: { + cv.slug: vol.All({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1), + [cv.string]), + vol.Optional(CONF_INITIAL): cv.string, + vol.Optional(CONF_ICON): cv.icon, + }, _cv_input_select)}}, required=True, extra=vol.ALLOW_EXTRA) + + def select_option(hass, entity_id, option): """Set input_select to False.""" hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, { @@ -44,39 +62,15 @@ def select_option(hass, entity_id, option): def setup(hass, config): """Setup input select.""" - if not isinstance(config.get(DOMAIN), dict): - _LOGGER.error('Expected %s config to be a dictionary', DOMAIN) - return False - component = EntityComponent(_LOGGER, DOMAIN, hass) entities = [] for object_id, cfg in config[DOMAIN].items(): - if object_id != slugify(object_id): - _LOGGER.warning("Found invalid key for boolean input: %s. " - "Use %s instead", object_id, slugify(object_id)) - continue - if not cfg: - _LOGGER.warning("No configuration specified for %s", object_id) - continue - name = cfg.get(CONF_NAME) options = cfg.get(CONF_OPTIONS) - - if not isinstance(options, list) or len(options) == 0: - _LOGGER.warning('Key %s should be a list of options', CONF_OPTIONS) - continue - - options = [str(val) for val in options] - - state = cfg.get(CONF_INITIAL) - - if state not in options: - state = options[0] - + state = cfg.get(CONF_INITIAL, options[0]) icon = cfg.get(CONF_ICON) - entities.append(InputSelect(object_id, name, state, options, icon)) if not entities: diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index 66b41d15c0c..91ebbd844fc 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -8,21 +8,19 @@ import logging import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.util import slugify DOMAIN = 'input_slider' ENTITY_ID_FORMAT = DOMAIN + '.{}' _LOGGER = logging.getLogger(__name__) -CONF_NAME = 'name' CONF_INITIAL = 'initial' CONF_MIN = 'min' CONF_MAX = 'max' -CONF_ICON = 'icon' CONF_STEP = 'step' ATTR_VALUE = 'value' @@ -38,6 +36,33 @@ SERVICE_SELECT_VALUE_SCHEMA = vol.Schema({ }) +def _cv_input_slider(cfg): + """Config validation helper for input slider (Voluptuous).""" + minimum = cfg.get(CONF_MIN) + maximum = cfg.get(CONF_MAX) + if minimum >= maximum: + raise vol.Invalid('Maximum ({}) is not greater than minimum ({})' + .format(minimum, maximum)) + state = cfg.get(CONF_INITIAL, minimum) + if state < minimum or state > maximum: + raise vol.Invalid('Initial value {} not in range {}-{}' + .format(state, minimum, maximum)) + cfg[CONF_INITIAL] = state + return cfg + +CONFIG_SCHEMA = vol.Schema({DOMAIN: { + cv.slug: vol.All({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_MIN): vol.Coerce(float), + vol.Required(CONF_MAX): vol.Coerce(float), + vol.Optional(CONF_INITIAL): vol.Coerce(float), + vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float), + vol.Range(min=1e-3)), + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string + }, _cv_input_slider)}}, required=True, extra=vol.ALLOW_EXTRA) + + def select_value(hass, entity_id, value): """Set input_slider to value.""" hass.services.call(DOMAIN, SERVICE_SELECT_VALUE, { @@ -48,36 +73,19 @@ def select_value(hass, entity_id, value): def setup(hass, config): """Set up input slider.""" - if not isinstance(config.get(DOMAIN), dict): - _LOGGER.error('Expected %s config to be a dictionary', DOMAIN) - return False - component = EntityComponent(_LOGGER, DOMAIN, hass) entities = [] for object_id, cfg in config[DOMAIN].items(): - if object_id != slugify(object_id): - _LOGGER.warning("Found invalid key for boolean input: %s. " - "Use %s instead", object_id, slugify(object_id)) - continue - if not cfg: - _LOGGER.warning("No configuration specified for %s", object_id) - continue - name = cfg.get(CONF_NAME) minimum = cfg.get(CONF_MIN) maximum = cfg.get(CONF_MAX) state = cfg.get(CONF_INITIAL, minimum) - step = cfg.get(CONF_STEP, 1) + step = cfg.get(CONF_STEP) icon = cfg.get(CONF_ICON) unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) - if state < minimum: - state = minimum - if state > maximum: - state = maximum - entities.append(InputSlider(object_id, name, state, minimum, maximum, step, icon, unit)) diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 22738ad0c13..2dc3af1dff9 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,12 +1,12 @@ """Helper methods for components within Home Assistant.""" import re -from typing import Any, Iterable, Tuple, List, Dict +from typing import Any, Iterable, Tuple, Sequence, Dict from homeassistant.const import CONF_PLATFORM # Typing Imports and TypeAlias -# pylint: disable=using-constant-test,unused-import +# pylint: disable=using-constant-test,unused-import,wrong-import-order if False: from logging import Logger # NOQA @@ -34,7 +34,7 @@ def config_per_platform(config: ConfigType, yield platform, item -def extract_domain_configs(config: ConfigType, domain: str) -> List[str]: +def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]: """Extract keys from config for given domain name.""" pattern = re.compile(r'^{}(| .+)$'.format(domain)) return [key for key in config.keys() if pattern.match(key)] diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 009736024a1..b7f81be66f3 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -4,7 +4,7 @@ from datetime import timedelta import os from urllib.parse import urlparse -from typing import Any, Union, TypeVar, Callable, Sequence, List, Dict +from typing import Any, Union, TypeVar, Callable, Sequence, Dict import jinja2 import voluptuous as vol @@ -80,7 +80,7 @@ def isfile(value: Any) -> str: return file_in -def ensure_list(value: Union[T, Sequence[T]]) -> List[T]: +def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]: """Wrap value in list if it is not one.""" return value if isinstance(value, list) else [value] @@ -93,7 +93,7 @@ def entity_id(value: Any) -> str: raise vol.Invalid('Entity ID {} is an invalid entity id'.format(value)) -def entity_ids(value: Union[str, Sequence]) -> List[str]: +def entity_ids(value: Union[str, Sequence]) -> Sequence[str]: """Validate Entity IDs.""" if value is None: raise vol.Invalid('Entity IDs can not be None') diff --git a/tests/components/test_input_boolean.py b/tests/components/test_input_boolean.py index 1ab4d59577c..cf5a33fc8ad 100644 --- a/tests/components/test_input_boolean.py +++ b/tests/components/test_input_boolean.py @@ -1,12 +1,17 @@ """The tests for the input_boolean component.""" # pylint: disable=too-many-public-methods,protected-access import unittest +import logging -from homeassistant.components import input_boolean +from tests.common import get_test_home_assistant + +from homeassistant.bootstrap import setup_component +from homeassistant.components.input_boolean import ( + DOMAIN, is_on, toggle, turn_off, turn_on) from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_ICON, ATTR_FRIENDLY_NAME) -from tests.common import get_test_home_assistant +_LOGGER = logging.getLogger(__name__) class TestInputBoolean(unittest.TestCase): @@ -22,68 +27,63 @@ class TestInputBoolean(unittest.TestCase): def test_config(self): """Test config.""" - self.assertFalse(input_boolean.setup(self.hass, { - 'input_boolean': None - })) + invalid_configs = [ + None, + 1, + {}, + {'name with space': None}, + ] - self.assertFalse(input_boolean.setup(self.hass, { - 'input_boolean': { - } - })) - - self.assertFalse(input_boolean.setup(self.hass, { - 'input_boolean': { - 'name with space': None - } - })) + for cfg in invalid_configs: + self.assertFalse( + setup_component(self.hass, DOMAIN, {DOMAIN: cfg})) def test_methods(self): """Test is_on, turn_on, turn_off methods.""" - self.assertTrue(input_boolean.setup(self.hass, { - 'input_boolean': { - 'test_1': None, - } - })) + self.assertTrue(setup_component(self.hass, DOMAIN, {DOMAIN: { + 'test_1': None, + }})) entity_id = 'input_boolean.test_1' self.assertFalse( - input_boolean.is_on(self.hass, entity_id)) + is_on(self.hass, entity_id)) - input_boolean.turn_on(self.hass, entity_id) + turn_on(self.hass, entity_id) self.hass.block_till_done() self.assertTrue( - input_boolean.is_on(self.hass, entity_id)) + is_on(self.hass, entity_id)) - input_boolean.turn_off(self.hass, entity_id) + turn_off(self.hass, entity_id) self.hass.block_till_done() self.assertFalse( - input_boolean.is_on(self.hass, entity_id)) + is_on(self.hass, entity_id)) - input_boolean.toggle(self.hass, entity_id) + toggle(self.hass, entity_id) self.hass.block_till_done() - self.assertTrue( - input_boolean.is_on(self.hass, entity_id)) + self.assertTrue(is_on(self.hass, entity_id)) def test_config_options(self): """Test configuration options.""" count_start = len(self.hass.states.entity_ids()) - self.assertTrue(input_boolean.setup(self.hass, { - 'input_boolean': { - 'test_1': None, - 'test_2': { - 'name': 'Hello World', - 'icon': 'work', - 'initial': True, - }, + _LOGGER.debug('ENTITIES @ start: %s', self.hass.states.entity_ids()) + + self.assertTrue(setup_component(self.hass, DOMAIN, {DOMAIN: { + 'test_1': None, + 'test_2': { + 'name': 'Hello World', + 'icon': 'mdi:work', + 'initial': True, }, - })) + }})) + + _LOGGER.debug('ENTITIES: %s', self.hass.states.entity_ids()) self.assertEqual(count_start + 2, len(self.hass.states.entity_ids())) @@ -100,4 +100,4 @@ class TestInputBoolean(unittest.TestCase): self.assertEqual(STATE_ON, state_2.state) self.assertEqual('Hello World', state_2.attributes.get(ATTR_FRIENDLY_NAME)) - self.assertEqual('work', state_2.attributes.get(ATTR_ICON)) + self.assertEqual('mdi:work', state_2.attributes.get(ATTR_ICON)) diff --git a/tests/components/test_input_select.py b/tests/components/test_input_select.py index 589a72952eb..a3f121576fb 100644 --- a/tests/components/test_input_select.py +++ b/tests/components/test_input_select.py @@ -2,12 +2,14 @@ # pylint: disable=too-many-public-methods,protected-access import unittest -from homeassistant.components import input_select +from tests.common import get_test_home_assistant + +from homeassistant.bootstrap import setup_component +from homeassistant.components.input_select import ( + ATTR_OPTIONS, DOMAIN, select_option) from homeassistant.const import ( ATTR_ICON, ATTR_FRIENDLY_NAME) -from tests.common import get_test_home_assistant - class TestInputSelect(unittest.TestCase): """Test the input select component.""" @@ -22,59 +24,44 @@ class TestInputSelect(unittest.TestCase): def test_config(self): """Test config.""" - self.assertFalse(input_select.setup(self.hass, { - 'input_select': None - })) + invalid_configs = [ + None, + {}, + {'name with space': None}, + # {'bad_options': {'options': None}}, + {'bad_initial': { + 'options': [1, 2], + 'initial': 3, + }}, + ] - self.assertFalse(input_select.setup(self.hass, { - 'input_select': { - } - })) - - self.assertFalse(input_select.setup(self.hass, { - 'input_select': { - 'name with space': None - } - })) - - self.assertFalse(input_select.setup(self.hass, { - 'input_select': { - 'hello': { - 'options': None - } - } - })) - - self.assertFalse(input_select.setup(self.hass, { - 'input_select': { - 'hello': None - } - })) + for cfg in invalid_configs: + self.assertFalse( + setup_component(self.hass, DOMAIN, {DOMAIN: cfg})) def test_select_option(self): """Test select_option methods.""" - self.assertTrue(input_select.setup(self.hass, { - 'input_select': { + self.assertTrue( + setup_component(self.hass, DOMAIN, {DOMAIN: { 'test_1': { 'options': [ 'some option', 'another option', ], }, - } - })) + }})) entity_id = 'input_select.test_1' state = self.hass.states.get(entity_id) self.assertEqual('some option', state.state) - input_select.select_option(self.hass, entity_id, 'another option') + select_option(self.hass, entity_id, 'another option') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual('another option', state.state) - input_select.select_option(self.hass, entity_id, 'non existing option') + select_option(self.hass, entity_id, 'non existing option') self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -90,8 +77,8 @@ class TestInputSelect(unittest.TestCase): 'Best Option', ] - self.assertTrue(input_select.setup(self.hass, { - 'input_select': { + self.assertTrue(setup_component(self.hass, DOMAIN, { + DOMAIN: { 'test_1': { 'options': [ 1, @@ -100,11 +87,11 @@ class TestInputSelect(unittest.TestCase): }, 'test_2': { 'name': 'Hello World', - 'icon': 'work', + 'icon': 'mdi:work', 'options': test_2_options, 'initial': 'Better Option', }, - }, + } })) self.assertEqual(count_start + 2, len(self.hass.states.entity_ids())) @@ -117,13 +104,12 @@ class TestInputSelect(unittest.TestCase): self.assertEqual('1', state_1.state) self.assertEqual(['1', '2'], - state_1.attributes.get(input_select.ATTR_OPTIONS)) + state_1.attributes.get(ATTR_OPTIONS)) self.assertNotIn(ATTR_ICON, state_1.attributes) - self.assertNotIn(ATTR_FRIENDLY_NAME, state_1.attributes) self.assertEqual('Better Option', state_2.state) self.assertEqual(test_2_options, - state_2.attributes.get(input_select.ATTR_OPTIONS)) + state_2.attributes.get(ATTR_OPTIONS)) self.assertEqual('Hello World', state_2.attributes.get(ATTR_FRIENDLY_NAME)) - self.assertEqual('work', state_2.attributes.get(ATTR_ICON)) + self.assertEqual('mdi:work', state_2.attributes.get(ATTR_ICON)) diff --git a/tests/components/test_input_slider.py b/tests/components/test_input_slider.py index 73ffcd9ba7a..85c6a6f08ca 100644 --- a/tests/components/test_input_slider.py +++ b/tests/components/test_input_slider.py @@ -2,10 +2,11 @@ # pylint: disable=too-many-public-methods,protected-access import unittest -from homeassistant.components import input_slider - from tests.common import get_test_home_assistant +from homeassistant.bootstrap import setup_component +from homeassistant.components.input_slider import (DOMAIN, select_value) + class TestInputSlider(unittest.TestCase): """Test the input slider component.""" @@ -20,50 +21,46 @@ class TestInputSlider(unittest.TestCase): def test_config(self): """Test config.""" - self.assertFalse(input_slider.setup(self.hass, { - 'input_slider': None - })) - - self.assertFalse(input_slider.setup(self.hass, { - 'input_slider': { - } - })) - - self.assertFalse(input_slider.setup(self.hass, { - 'input_slider': { - 'name with space': None - } - })) + invalid_configs = [ + None, + {}, + {'name with space': None}, + {'test_1': { + 'min': 50, + 'max': 50, + }}, + ] + for cfg in invalid_configs: + self.assertFalse( + setup_component(self.hass, DOMAIN, {DOMAIN: cfg})) def test_select_value(self): """Test select_value method.""" - self.assertTrue(input_slider.setup(self.hass, { - 'input_slider': { - 'test_1': { - 'initial': 50, - 'min': 0, - 'max': 100, - }, - } - })) + self.assertTrue(setup_component(self.hass, DOMAIN, {DOMAIN: { + 'test_1': { + 'initial': 50, + 'min': 0, + 'max': 100, + }, + }})) entity_id = 'input_slider.test_1' state = self.hass.states.get(entity_id) self.assertEqual(50, float(state.state)) - input_slider.select_value(self.hass, entity_id, '30.4') + select_value(self.hass, entity_id, '30.4') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual(30.4, float(state.state)) - input_slider.select_value(self.hass, entity_id, '70') + select_value(self.hass, entity_id, '70') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual(70, float(state.state)) - input_slider.select_value(self.hass, entity_id, '110') + select_value(self.hass, entity_id, '110') self.hass.block_till_done() state = self.hass.states.get(entity_id)