From 5baa98b79f1ed9b267cdf3d500b43a5826dc14b3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Mar 2016 18:48:51 -0700 Subject: [PATCH] Add initial config validation --- homeassistant/__main__.py | 3 + homeassistant/bootstrap.py | 68 +++++++---- .../components/binary_sensor/__init__.py | 1 + homeassistant/components/camera/__init__.py | 1 + .../components/device_tracker/__init__.py | 4 +- .../components/garage_door/__init__.py | 2 +- homeassistant/components/group.py | 43 +++++-- homeassistant/components/light/__init__.py | 1 + homeassistant/components/lock/__init__.py | 2 +- .../components/media_player/__init__.py | 1 + homeassistant/components/notify/__init__.py | 6 +- .../components/rollershutter/__init__.py | 1 + homeassistant/components/sensor/__init__.py | 1 + homeassistant/components/switch/__init__.py | 2 +- .../components/thermostat/__init__.py | 1 + homeassistant/config.py | 32 ++++- homeassistant/helpers/__init__.py | 16 +-- homeassistant/helpers/config_validation.py | 65 ++++++++++ homeassistant/helpers/entity_component.py | 4 +- requirements_all.txt | 1 + setup.py | 1 + tests/components/test_group.py | 23 ++-- tests/helpers/test_config_validation.py | 114 ++++++++++++++++++ tests/test_config.py | 28 +++++ 24 files changed, 349 insertions(+), 72 deletions(-) create mode 100644 homeassistant/helpers/config_validation.py create mode 100644 tests/helpers/test_config_validation.py diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 5e21119d91fa..6dbb609ec05f 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -244,6 +244,9 @@ def setup_and_run_hass(config_dir, args, top_process=False): config_file, daemon=args.daemon, verbose=args.verbose, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days) + if hass is None: + return + if args.open_ui: def open_browser(event): """Open the webinterface in a browser.""" diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c6f1a75b8a62..6c4334e314f1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -8,6 +8,8 @@ import sys from collections import defaultdict from threading import RLock +import voluptuous as vol + import homeassistant.components as core_components import homeassistant.components.group as group import homeassistant.config as config_util @@ -20,7 +22,8 @@ from homeassistant.const import ( CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED, TEMP_CELCIUS, TEMP_FAHRENHEIT, __version__) -from homeassistant.helpers import event_decorators, service +from homeassistant.helpers import ( + event_decorators, service, config_per_platform) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -72,7 +75,7 @@ def _handle_requirements(hass, component, name): def _setup_component(hass, domain, config): """Setup a component for Home Assistant.""" - # pylint: disable=too-many-return-statements + # pylint: disable=too-many-return-statements,too-many-branches if domain in hass.config.components: return True @@ -96,6 +99,25 @@ def _setup_component(hass, domain, config): domain, ", ".join(missing_deps)) return False + if hasattr(component, 'CONFIG_SCHEMA'): + try: + config = component.CONFIG_SCHEMA(config) + except vol.MultipleInvalid as ex: + _LOGGER.error('Invalid config for [%s]: %s', domain, ex) + return False + + if hasattr(component, 'PLATFORM_SCHEMA'): + platforms = [] + for _, platform in config_per_platform(config, domain): + try: + platforms.append(component.PLATFORM_SCHEMA(platform)) + except vol.MultipleInvalid as ex: + _LOGGER.error('Invalid platform config for [%s]: %s. %s', + domain, ex, platform) + return False + + config = {domain: platforms} + if not _handle_requirements(hass, component, domain): return False @@ -176,8 +198,14 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, hass.config.config_dir = config_dir mount_local_lib_path(config_dir) + try: + process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA( + config.get(core.DOMAIN, {}))) + except vol.MultipleInvalid as ex: + _LOGGER.error('Invalid config for [homeassistant]: %s', ex) + return None + process_ha_config_upgrade(hass) - process_ha_core_config(hass, config.get(core.DOMAIN, {})) if enable_log: enable_logging(hass, verbose, daemon, log_rotate_days) @@ -336,40 +364,28 @@ def process_ha_core_config(hass, config): else: _LOGGER.error('Received invalid time zone %s', time_zone_str) - for key, attr, typ in ((CONF_LATITUDE, 'latitude', float), - (CONF_LONGITUDE, 'longitude', float), - (CONF_NAME, 'location_name', str)): + for key, attr in ((CONF_LATITUDE, 'latitude'), + (CONF_LONGITUDE, 'longitude'), + (CONF_NAME, 'location_name')): if key in config: - try: - setattr(hac, attr, typ(config[key])) - except ValueError: - _LOGGER.error('Received invalid %s value for %s: %s', - typ.__name__, key, attr) + setattr(hac, attr, config[key]) - set_time_zone(config.get(CONF_TIME_ZONE)) + if CONF_TIME_ZONE in config: + set_time_zone(config.get(CONF_TIME_ZONE)) - customize = config.get(CONF_CUSTOMIZE) - - if isinstance(customize, dict): - for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items(): - if not isinstance(attrs, dict): - continue - Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) + for entity_id, attrs in config.get(CONF_CUSTOMIZE).items(): + Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) if CONF_TEMPERATURE_UNIT in config: - unit = config[CONF_TEMPERATURE_UNIT] - - if unit == 'C': - hac.temperature_unit = TEMP_CELCIUS - elif unit == 'F': - hac.temperature_unit = TEMP_FAHRENHEIT + hac.temperature_unit = config[CONF_TEMPERATURE_UNIT] # If we miss some of the needed values, auto detect them if None not in ( hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone): return - _LOGGER.info('Auto detecting location and temperature unit') + _LOGGER.warning('Incomplete core config. Auto detecting location and ' + 'temperature unit') info = loc_util.detect_location_info() diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 0fed26bd7102..8d941166fddf 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.const import (STATE_ON, STATE_OFF) from homeassistant.components import ( bloomsky, mysensors, zwave, vera, wemo, wink) +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa DOMAIN = 'binary_sensor' SCAN_INTERVAL = 30 diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d48563d471f4..c473f159f652 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -15,6 +15,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.components import bloomsky from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa DOMAIN = 'camera' diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 4278d3db6555..240ac532e5c8 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -17,6 +17,7 @@ from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform from homeassistant.helpers.entity import Entity +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.util as util import homeassistant.util.dt as dt_util @@ -129,8 +130,7 @@ def setup(hass, config): except Exception: # pylint: disable=broad-except _LOGGER.exception('Error setting up platform %s', p_type) - for p_type, p_config in \ - config_per_platform(config, DOMAIN, _LOGGER): + for p_type, p_config in config_per_platform(config, DOMAIN): setup_platform(p_type, p_config) def device_tracker_discovered(service, info): diff --git a/homeassistant/components/garage_door/__init__.py b/homeassistant/components/garage_door/__init__.py index aace95d8a055..736949e474e9 100644 --- a/homeassistant/components/garage_door/__init__.py +++ b/homeassistant/components/garage_door/__init__.py @@ -10,7 +10,7 @@ import os from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity - +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.const import ( STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN, ATTR_ENTITY_ID) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 028222e0e763..392f6dbd92a8 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -6,6 +6,8 @@ https://home-assistant.io/components/group/ """ import threading +import voluptuous as vol + import homeassistant.core as ha from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, @@ -14,6 +16,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import ( Entity, generate_entity_id, split_entity_id) from homeassistant.helpers.event import track_state_change +import homeassistant.helpers.config_validation as cv DOMAIN = 'group' @@ -26,6 +29,38 @@ ATTR_AUTO = 'auto' ATTR_ORDER = 'order' ATTR_VIEW = 'view' + +def _conf_preprocess(value): + """Preprocess alternative configuration formats.""" + if isinstance(value, (str, list)): + value = {CONF_ENTITIES: value} + + return value + +_SINGLE_GROUP_CONFIG = vol.Schema(vol.All(_conf_preprocess, { + vol.Required(CONF_ENTITIES): cv.entity_ids, + CONF_VIEW: bool, + CONF_NAME: str, + CONF_ICON: cv.icon, +})) + + +def _group_dict(value): + """Validate a dictionary of group definitions.""" + config = {} + for key, group in value.items(): + try: + config[key] = _SINGLE_GROUP_CONFIG(group) + except vol.MultipleInvalid as ex: + raise vol.Invalid('Group {} is invalid: {}'.format(key, ex)) + + return config + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(dict, _group_dict) +}, extra=True) + # List of ON/OFF state tuples for groupable states _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME), (STATE_OPEN, STATE_CLOSED)] @@ -108,17 +143,11 @@ def get_entity_ids(hass, entity_id, domain_filter=None): def setup(hass, config): """Setup all groups found definded in the configuration.""" for object_id, conf in config.get(DOMAIN, {}).items(): - if not isinstance(conf, dict): - conf = {CONF_ENTITIES: conf} - name = conf.get(CONF_NAME, object_id) - entity_ids = conf.get(CONF_ENTITIES) + entity_ids = conf[CONF_ENTITIES] icon = conf.get(CONF_ICON) view = conf.get(CONF_VIEW) - if isinstance(entity_ids, str): - entity_ids = [ent.strip() for ent in entity_ids.split(",")] - Group(hass, name, entity_ids, icon=icon, view=view, object_id=object_id) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index fa3aaa805962..c7014fd05f6d 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.util as util import homeassistant.util.color as color_util diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 6d21ae310d88..821e2e3da335 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -11,7 +11,7 @@ import os from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity - +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index a0d654133de5..a2f4b7c52838 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -11,6 +11,7 @@ from homeassistant.components import discovery from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.const import ( STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index d142c5a29d16..f9b3a5b940bd 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -10,8 +10,8 @@ import os import homeassistant.bootstrap as bootstrap from homeassistant.config import load_yaml_config_file -from homeassistant.helpers import config_per_platform -from homeassistant.helpers import template +from homeassistant.helpers import config_per_platform, template +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.const import CONF_NAME @@ -51,7 +51,7 @@ def setup(hass, config): descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) - for platform, p_config in config_per_platform(config, DOMAIN, _LOGGER): + for platform, p_config in config_per_platform(config, DOMAIN): notify_implementation = bootstrap.prepare_setup_platform( hass, config, DOMAIN, platform) diff --git a/homeassistant/components/rollershutter/__init__.py b/homeassistant/components/rollershutter/__init__.py index 109302708b26..5c90aa99fe29 100644 --- a/homeassistant/components/rollershutter/__init__.py +++ b/homeassistant/components/rollershutter/__init__.py @@ -10,6 +10,7 @@ import logging from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components import group from homeassistant.const import ( SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_STOP, diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 48dee4e169b0..3c625ae4a85f 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/sensor/ import logging from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components import ( wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors, bloomsky, vera) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index ac19a1a76394..d807668fb730 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -11,7 +11,7 @@ import os from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity - +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 9e128f956301..c6ddbeb0df40 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -13,6 +13,7 @@ from homeassistant.config import load_yaml_config_file import homeassistant.util as util from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import convert +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components import ecobee from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, diff --git a/homeassistant/config.py b/homeassistant/config.py index f18ab0fe0375..09f75ce69765 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,13 +1,18 @@ """Module to help with parsing and generating configuration files.""" import logging import os +from types import MappingProxyType + +import voluptuous as vol import homeassistant.util.location as loc_util from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT, - CONF_TIME_ZONE) + CONF_TIME_ZONE, CONF_CUSTOMIZE) from homeassistant.exceptions import HomeAssistantError from homeassistant.util.yaml import load_yaml +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import valid_entity_id _LOGGER = logging.getLogger(__name__) @@ -37,6 +42,31 @@ DEFAULT_COMPONENTS = { } +def _valid_customize(value): + """Config validator for customize.""" + if not isinstance(value, dict): + raise vol.Invalid('Expected dictionary') + + for key, val in value.items(): + if not valid_entity_id(key): + raise vol.Invalid('Invalid entity ID: {}'.format(key)) + + if not isinstance(val, dict): + raise vol.Invalid('Value of {} is not a dictionary'.format(key)) + + return value + +CORE_CONFIG_SCHEMA = vol.Schema({ + CONF_NAME: vol.Coerce(str), + CONF_LATITUDE: cv.latitude, + CONF_LONGITUDE: cv.longitude, + CONF_TEMPERATURE_UNIT: cv.temperature_unit, + CONF_TIME_ZONE: cv.time_zone, + vol.Required(CONF_CUSTOMIZE, + default=MappingProxyType({})): _valid_customize, +}) + + def get_default_config_dir(): """Put together the default configuration directory based on OS.""" data_dir = os.getenv('APPDATA') if os.name == "nt" \ diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 07e14c260fc4..12d15e2bcd47 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -29,30 +29,18 @@ def validate_config(config, items, logger): return not errors_found -def config_per_platform(config, domain, logger): +def config_per_platform(config, domain): """Generator to break a component config into different platforms. For example, will find 'switch', 'switch 2', 'switch 3', .. etc """ - config_key = domain - found = 1 - for config_key in extract_domain_configs(config, domain): platform_config = config[config_key] if not isinstance(platform_config, list): platform_config = [platform_config] for item in platform_config: - platform_type = item.get(CONF_PLATFORM) - - if platform_type is None: - logger.warning('No platform specified for %s', config_key) - continue - - yield platform_type, item - - found += 1 - config_key = "{} {}".format(domain, found) + yield item.get(CONF_PLATFORM), item def extract_domain_configs(config, domain): diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py new file mode 100644 index 000000000000..1b83b2ee8791 --- /dev/null +++ b/homeassistant/helpers/config_validation.py @@ -0,0 +1,65 @@ +"""Helpers for config validation using voluptuous.""" +import voluptuous as vol + +from homeassistant.const import ( + CONF_PLATFORM, TEMP_CELCIUS, TEMP_FAHRENHEIT) +from homeassistant.helpers.entity import valid_entity_id +import homeassistant.util.dt as dt_util + +# pylint: disable=invalid-name + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): str, +}, extra=vol.ALLOW_EXTRA) + +latitude = vol.All(vol.Coerce(float), vol.Range(min=-90, max=90)) +longitude = vol.All(vol.Coerce(float), vol.Range(min=-180, max=180)) + + +def entity_id(value): + """Validate Entity ID.""" + if valid_entity_id(value): + return value + raise vol.Invalid('Entity ID {} does not match format .' + .format(value)) + + +def entity_ids(value): + """Validate Entity IDs.""" + if isinstance(value, str): + value = [ent_id.strip() for ent_id in value.split(',')] + + for ent_id in value: + entity_id(ent_id) + + return value + + +def icon(value): + """Validate icon.""" + value = str(value) + + if value.startswith('mdi:'): + return value + + raise vol.Invalid('Icons should start with prefix "mdi:"') + + +def temperature_unit(value): + """Validate and transform temperature unit.""" + if isinstance(value, str): + value = value.upper() + if value == 'C': + return TEMP_CELCIUS + elif value == 'F': + return TEMP_FAHRENHEIT + raise vol.Invalid('Invalid temperature unit. Expected: C or F') + + +def time_zone(value): + """Validate timezone.""" + if dt_util.get_time_zone(value) is not None: + return value + raise vol.Invalid( + 'Invalid time zone passed in. Valid options can be found here: ' + 'http://en.wikipedia.org/wiki/List_of_tz_database_time_zones') diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 46b9ef284b39..b1a18b1de37f 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -49,9 +49,7 @@ class EntityComponent(object): self.config = config # Look in config for Domain, Domain 2, Domain 3 etc and load them - for p_type, p_config in \ - config_per_platform(config, self.domain, self.logger): - + for p_type, p_config in config_per_platform(config, self.domain): self._setup_platform(p_type, p_config) if self.discovery_platforms: diff --git a/requirements_all.txt b/requirements_all.txt index 8058062fac68..289ad2491eef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,6 +5,7 @@ pytz>=2015.4 pip>=7.0.0 vincenty==0.1.3 jinja2>=2.8 +voluptuous==0.8.9 # homeassistant.components.isy994 PyISY==1.0.5 diff --git a/setup.py b/setup.py index 9cc79615c712..7ddc2ab588e9 100755 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ REQUIRES = [ 'pip>=7.0.0', 'vincenty==0.1.3', 'jinja2>=2.8', + 'voluptuous==0.8.9', ] setup( diff --git a/tests/components/test_group.py b/tests/components/test_group.py index 3b4f3d43c29c..8526be9768d3 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access,too-many-public-methods import unittest +from homeassistant.bootstrap import _setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN, ATTR_ICON, ATTR_HIDDEN, ATTR_ASSUMED_STATE, ) @@ -218,19 +219,15 @@ class TestComponentsGroup(unittest.TestCase): test_group = group.Group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) - self.assertTrue( - group.setup( - self.hass, - { - group.DOMAIN: { - 'second_group': { - 'entities': 'light.Bowl, ' + test_group.entity_id, - 'icon': 'mdi:work', - 'view': True, - }, - 'test_group': 'hello.world,sensor.happy', - } - })) + _setup_component(self.hass, 'group', {'group': { + 'second_group': { + 'entities': 'light.Bowl, ' + test_group.entity_id, + 'icon': 'mdi:work', + 'view': True, + }, + 'test_group': 'hello.world,sensor.happy', + } + }) group_state = self.hass.states.get( group.ENTITY_ID_FORMAT.format('second_group')) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py new file mode 100644 index 000000000000..25d0e7e28ff3 --- /dev/null +++ b/tests/helpers/test_config_validation.py @@ -0,0 +1,114 @@ +import pytest +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + + +def test_latitude(): + """Test latitude validation.""" + schema = vol.Schema(cv.latitude) + + for value in ('invalid', None, -91, 91, '-91', '91', '123.01A'): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ('-89', 89, '12.34'): + schema(value) + + +def test_longitude(): + """Test longitude validation.""" + schema = vol.Schema(cv.longitude) + + for value in ('invalid', None, -181, 181, '-181', '181', '123.01A'): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ('-179', 179, '12.34'): + schema(value) + + +def test_icon(): + """Test icon validation.""" + schema = vol.Schema(cv.icon) + + for value in (False, 'work', 'icon:work'): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + schema('mdi:work') + + +def test_platform_config(): + """Test platform config validation.""" + for value in ( + {'platform': 1}, + {}, + {'hello': 'world'}, + ): + with pytest.raises(vol.MultipleInvalid): + cv.PLATFORM_SCHEMA(value) + + for value in ( + {'platform': 'mqtt'}, + {'platform': 'mqtt', 'beer': 'yes'}, + ): + cv.PLATFORM_SCHEMA(value) + + +def test_entity_id(): + """Test entity ID validation.""" + schema = vol.Schema(cv.entity_id) + + with pytest.raises(vol.MultipleInvalid): + schema('invalid_entity') + + schema('sensor.light') + + +def test_entity_ids(): + """Test entity ID validation.""" + schema = vol.Schema(cv.entity_ids) + + for value in ( + 'invalid_entity', + 'sensor.light,sensor_invalid', + ['invalid_entity'], + ['sensor.light', 'sensor_invalid'], + ['sensor.light,sensor_invalid'], + ): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ( + [], + ['sensor.light'], + 'sensor.light' + ): + schema(value) + + assert schema('sensor.light, light.kitchen ') == [ + 'sensor.light', 'light.kitchen' + ] + + +def test_temperature_unit(): + """Test temperature unit validation.""" + schema = vol.Schema(cv.temperature_unit) + + with pytest.raises(vol.MultipleInvalid): + schema('K') + + schema('C') + schema('F') + + +def test_time_zone(): + """Test time zone validation.""" + schema = vol.Schema(cv.time_zone) + + with pytest.raises(vol.MultipleInvalid): + schema('America/Do_Not_Exist') + + schema('America/Los_Angeles') + schema('UTC') diff --git a/tests/test_config.py b/tests/test_config.py index 8ce6369d6d24..8a5ec306b3be 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,6 +4,9 @@ import unittest import unittest.mock as mock import os +import pytest +from voluptuous import MultipleInvalid + from homeassistant.core import DOMAIN, HomeAssistantError import homeassistant.config as config_util from homeassistant.const import ( @@ -138,3 +141,28 @@ class TestConfig(unittest.TestCase): config_util.create_default_config( os.path.join(CONFIG_DIR, 'non_existing_dir/'), False)) self.assertTrue(mock_print.called) + + def test_core_config_schema(self): + for value in ( + {'temperature_unit': 'K'}, + {'time_zone': 'non-exist'}, + {'latitude': '91'}, + {'longitude': -181}, + {'customize': 'bla'}, + {'customize': {'invalid_entity_id': {}}}, + {'customize': {'light.sensor': 100}}, + ): + with pytest.raises(MultipleInvalid): + config_util.CORE_CONFIG_SCHEMA(value) + + config_util.CORE_CONFIG_SCHEMA({ + 'name': 'Test name', + 'latitude': '-23.45', + 'longitude': '123.45', + 'temperature_unit': 'c', + 'customize': { + 'sensor.temperature': { + 'hidden': True, + }, + }, + })