Merge pull request #1633 from balloob/config-validation

Add initial config validation
This commit is contained in:
Paulus Schoutsen 2016-03-29 23:01:36 -07:00
commit a4ffec341b
27 changed files with 574 additions and 105 deletions

View File

@ -244,6 +244,9 @@ def setup_and_run_hass(config_dir, args, top_process=False):
config_file, daemon=args.daemon, verbose=args.verbose, config_file, daemon=args.daemon, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days) skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
if hass is None:
return
if args.open_ui: if args.open_ui:
def open_browser(event): def open_browser(event):
"""Open the webinterface in a browser.""" """Open the webinterface in a browser."""

View File

@ -8,6 +8,8 @@ import sys
from collections import defaultdict from collections import defaultdict
from threading import RLock from threading import RLock
import voluptuous as vol
import homeassistant.components as core_components import homeassistant.components as core_components
import homeassistant.components.group as group import homeassistant.components.group as group
import homeassistant.config as config_util import homeassistant.config as config_util
@ -20,7 +22,8 @@ from homeassistant.const import (
CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME,
CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED, CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED,
TEMP_CELCIUS, TEMP_FAHRENHEIT, __version__) 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 from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -72,7 +75,7 @@ def _handle_requirements(hass, component, name):
def _setup_component(hass, domain, config): def _setup_component(hass, domain, config):
"""Setup a component for Home Assistant.""" """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: if domain in hass.config.components:
return True return True
@ -96,6 +99,25 @@ def _setup_component(hass, domain, config):
domain, ", ".join(missing_deps)) domain, ", ".join(missing_deps))
return False 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
elif 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): if not _handle_requirements(hass, component, domain):
return False 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 hass.config.config_dir = config_dir
mount_local_lib_path(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_config_upgrade(hass)
process_ha_core_config(hass, config.get(core.DOMAIN, {}))
if enable_log: if enable_log:
enable_logging(hass, verbose, daemon, log_rotate_days) enable_logging(hass, verbose, daemon, log_rotate_days)
@ -262,8 +290,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
} }
)) ))
except ImportError: except ImportError:
_LOGGER.warning( pass
"Colorlog package not found, console coloring disabled")
# Log errors to a file if we have write access to file or config dir # Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path(ERROR_LOG_FILENAME) err_log_path = hass.config.path(ERROR_LOG_FILENAME)
@ -336,40 +363,28 @@ def process_ha_core_config(hass, config):
else: else:
_LOGGER.error('Received invalid time zone %s', time_zone_str) _LOGGER.error('Received invalid time zone %s', time_zone_str)
for key, attr, typ in ((CONF_LATITUDE, 'latitude', float), for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude', float), (CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name', str)): (CONF_NAME, 'location_name')):
if key in config: if key in config:
try: setattr(hac, attr, config[key])
setattr(hac, attr, typ(config[key]))
except ValueError:
_LOGGER.error('Received invalid %s value for %s: %s',
typ.__name__, key, attr)
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) for entity_id, attrs in config.get(CONF_CUSTOMIZE).items():
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
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())
if CONF_TEMPERATURE_UNIT in config: if CONF_TEMPERATURE_UNIT in config:
unit = config[CONF_TEMPERATURE_UNIT] hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
if unit == 'C':
hac.temperature_unit = TEMP_CELCIUS
elif unit == 'F':
hac.temperature_unit = TEMP_FAHRENHEIT
# If we miss some of the needed values, auto detect them # If we miss some of the needed values, auto detect them
if None not in ( if None not in (
hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone): hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone):
return 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() info = loc_util.detect_location_info()

View File

@ -11,6 +11,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_ON, STATE_OFF) from homeassistant.const import (STATE_ON, STATE_OFF)
from homeassistant.components import ( from homeassistant.components import (
bloomsky, mysensors, zwave, vera, wemo, wink) bloomsky, mysensors, zwave, vera, wemo, wink)
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
DOMAIN = 'binary_sensor' DOMAIN = 'binary_sensor'
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30

View File

@ -15,6 +15,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import bloomsky from homeassistant.components import bloomsky
from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
DOMAIN = 'camera' DOMAIN = 'camera'

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/conversation/
""" """
import logging import logging
import re import re
import warnings
from homeassistant import core from homeassistant import core
from homeassistant.const import ( from homeassistant.const import (
@ -24,6 +25,7 @@ REQUIREMENTS = ['fuzzywuzzy==0.8.0']
def setup(hass, config): def setup(hass, config):
"""Register the process service.""" """Register the process service."""
warnings.filterwarnings('ignore', module='fuzzywuzzy')
from fuzzywuzzy import process as fuzzyExtract from fuzzywuzzy import process as fuzzyExtract
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -17,6 +17,7 @@ from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform from homeassistant.helpers import config_per_platform
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.util as util import homeassistant.util as util
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -129,8 +130,7 @@ def setup(hass, config):
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error setting up platform %s', p_type) _LOGGER.exception('Error setting up platform %s', p_type)
for p_type, p_config in \ for p_type, p_config in config_per_platform(config, DOMAIN):
config_per_platform(config, DOMAIN, _LOGGER):
setup_platform(p_type, p_config) setup_platform(p_type, p_config)
def device_tracker_discovered(service, info): def device_tracker_discovered(service, info):

View File

@ -10,7 +10,7 @@ import os
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.const import ( from homeassistant.const import (
STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN, STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN,
ATTR_ENTITY_ID) ATTR_ENTITY_ID)

View File

@ -6,6 +6,8 @@ https://home-assistant.io/components/group/
""" """
import threading import threading
import voluptuous as vol
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
@ -14,6 +16,7 @@ from homeassistant.const import (
from homeassistant.helpers.entity import ( from homeassistant.helpers.entity import (
Entity, generate_entity_id, split_entity_id) Entity, generate_entity_id, split_entity_id)
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv
DOMAIN = 'group' DOMAIN = 'group'
@ -26,6 +29,38 @@ ATTR_AUTO = 'auto'
ATTR_ORDER = 'order' ATTR_ORDER = 'order'
ATTR_VIEW = 'view' 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 # List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME), _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
(STATE_OPEN, STATE_CLOSED)] (STATE_OPEN, STATE_CLOSED)]
@ -108,17 +143,11 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
def setup(hass, config): def setup(hass, config):
"""Setup all groups found definded in the configuration.""" """Setup all groups found definded in the configuration."""
for object_id, conf in config.get(DOMAIN, {}).items(): 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) name = conf.get(CONF_NAME, object_id)
entity_ids = conf.get(CONF_ENTITIES) entity_ids = conf[CONF_ENTITIES]
icon = conf.get(CONF_ICON) icon = conf.get(CONF_ICON)
view = conf.get(CONF_VIEW) 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, Group(hass, name, entity_ids, icon=icon, view=view,
object_id=object_id) object_id=object_id)

View File

@ -17,6 +17,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID) ATTR_ENTITY_ID)
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.util as util import homeassistant.util as util
import homeassistant.util.color as color_util import homeassistant.util.color as color_util

View File

@ -11,7 +11,7 @@ import os
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK)

View File

@ -11,6 +11,7 @@ from homeassistant.components import discovery
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.const import ( from homeassistant.const import (
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE, STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,

View File

@ -10,8 +10,8 @@ import os
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_per_platform from homeassistant.helpers import config_per_platform, template
from homeassistant.helpers import template from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
@ -51,7 +51,7 @@ def setup(hass, config):
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml')) 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( notify_implementation = bootstrap.prepare_setup_platform(
hass, config, DOMAIN, platform) hass, config, DOMAIN, platform)

View File

@ -10,6 +10,7 @@ import logging
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import group from homeassistant.components import group
from homeassistant.const import ( from homeassistant.const import (
SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_STOP, SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_STOP,

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/sensor/
import logging import logging
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import ( from homeassistant.components import (
wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors, wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors,
bloomsky, vera) bloomsky, vera)

View File

@ -11,7 +11,7 @@ import os
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
ATTR_ENTITY_ID) ATTR_ENTITY_ID)

View File

@ -13,6 +13,7 @@ from homeassistant.config import load_yaml_config_file
import homeassistant.util as util import homeassistant.util as util
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import convert from homeassistant.helpers.temperature import convert
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import ecobee from homeassistant.components import ecobee
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,

View File

@ -1,13 +1,18 @@
"""Module to help with parsing and generating configuration files.""" """Module to help with parsing and generating configuration files."""
import logging import logging
import os import os
from types import MappingProxyType
import voluptuous as vol
import homeassistant.util.location as loc_util import homeassistant.util.location as loc_util
from homeassistant.const import ( from homeassistant.const import (
CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT,
CONF_TIME_ZONE) CONF_TIME_ZONE, CONF_CUSTOMIZE)
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.yaml import load_yaml 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__) _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(): def get_default_config_dir():
"""Put together the default configuration directory based on OS.""" """Put together the default configuration directory based on OS."""
data_dir = os.getenv('APPDATA') if os.name == "nt" \ data_dir = os.getenv('APPDATA') if os.name == "nt" \

View File

@ -29,30 +29,19 @@ def validate_config(config, items, logger):
return not errors_found 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. """Generator to break a component config into different platforms.
For example, will find 'switch', 'switch 2', 'switch 3', .. etc For example, will find 'switch', 'switch 2', 'switch 3', .. etc
""" """
config_key = domain
found = 1
for config_key in extract_domain_configs(config, domain): for config_key in extract_domain_configs(config, domain):
platform_config = config[config_key] platform_config = config[config_key]
if not isinstance(platform_config, list): if not isinstance(platform_config, list):
platform_config = [platform_config] platform_config = [platform_config]
for item in platform_config: for item in platform_config:
platform_type = item.get(CONF_PLATFORM) platform = None if item is None else item.get(CONF_PLATFORM)
yield platform, item
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)
def extract_domain_configs(config, domain): def extract_domain_configs(config, domain):

View File

@ -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 <domain>.<object_id>'
.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')

View File

@ -49,9 +49,7 @@ class EntityComponent(object):
self.config = config self.config = config
# Look in config for Domain, Domain 2, Domain 3 etc and load them # Look in config for Domain, Domain 2, Domain 3 etc and load them
for p_type, p_config in \ for p_type, p_config in config_per_platform(config, self.domain):
config_per_platform(config, self.domain, self.logger):
self._setup_platform(p_type, p_config) self._setup_platform(p_type, p_config)
if self.discovery_platforms: if self.discovery_platforms:

View File

@ -5,6 +5,7 @@ pytz>=2015.4
pip>=7.0.0 pip>=7.0.0
vincenty==0.1.3 vincenty==0.1.3
jinja2>=2.8 jinja2>=2.8
voluptuous==0.8.9
# homeassistant.components.isy994 # homeassistant.components.isy994
PyISY==1.0.5 PyISY==1.0.5

View File

@ -17,6 +17,7 @@ REQUIRES = [
'pip>=7.0.0', 'pip>=7.0.0',
'vincenty==0.1.3', 'vincenty==0.1.3',
'jinja2>=2.8', 'jinja2>=2.8',
'voluptuous==0.8.9',
] ]
setup( setup(

View File

@ -143,10 +143,19 @@ class MockHTTP(object):
class MockModule(object): class MockModule(object):
"""Representation of a fake module.""" """Representation of a fake module."""
def __init__(self, domain=None, dependencies=[], setup=None): def __init__(self, domain=None, dependencies=[], setup=None,
requirements=[], config_schema=None, platform_schema=None):
"""Initialize the mock module.""" """Initialize the mock module."""
self.DOMAIN = domain self.DOMAIN = domain
self.DEPENDENCIES = dependencies self.DEPENDENCIES = dependencies
self.REQUIREMENTS = requirements
if config_schema is not None:
self.CONFIG_SCHEMA = config_schema
if platform_schema is not None:
self.PLATFORM_SCHEMA = platform_schema
# Setup a mock setup if none given. # Setup a mock setup if none given.
if setup is None: if setup is None:
self.setup = lambda hass, config: True self.setup = lambda hass, config: True

View File

@ -2,6 +2,7 @@
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import unittest import unittest
from homeassistant.bootstrap import _setup_component
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN, ATTR_ICON, ATTR_HIDDEN, STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN, ATTR_ICON, ATTR_HIDDEN,
ATTR_ASSUMED_STATE, ) ATTR_ASSUMED_STATE, )
@ -218,19 +219,15 @@ class TestComponentsGroup(unittest.TestCase):
test_group = group.Group( test_group = group.Group(
self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False)
self.assertTrue( _setup_component(self.hass, 'group', {'group': {
group.setup( 'second_group': {
self.hass, 'entities': 'light.Bowl, ' + test_group.entity_id,
{ 'icon': 'mdi:work',
group.DOMAIN: { 'view': True,
'second_group': { },
'entities': 'light.Bowl, ' + test_group.entity_id, 'test_group': 'hello.world,sensor.happy',
'icon': 'mdi:work', }
'view': True, })
},
'test_group': 'hello.world,sensor.happy',
}
}))
group_state = self.hass.states.get( group_state = self.hass.states.get(
group.ENTITY_ID_FORMAT.format('second_group')) group.ENTITY_ID_FORMAT.format('second_group'))

View File

@ -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')

View File

@ -2,27 +2,43 @@
# pylint: disable=too-many-public-methods,protected-access # pylint: disable=too-many-public-methods,protected-access
import os import os
import tempfile import tempfile
import unittest from unittest import mock
import threading
import voluptuous as vol
from homeassistant import bootstrap, loader from homeassistant import bootstrap, loader
from homeassistant.const import (__version__, CONF_LATITUDE, CONF_LONGITUDE, from homeassistant.const import (__version__, CONF_LATITUDE, CONF_LONGITUDE,
CONF_NAME, CONF_CUSTOMIZE) CONF_NAME, CONF_CUSTOMIZE)
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from tests.common import get_test_home_assistant, MockModule from tests.common import get_test_home_assistant, MockModule
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
class TestBootstrap(unittest.TestCase):
class TestBootstrap:
"""Test the bootstrap utils.""" """Test the bootstrap utils."""
def setUp(self): def setup_method(self, method):
"""Setup the test.""" """Setup the test."""
self.orig_timezone = dt_util.DEFAULT_TIME_ZONE if method == self.test_from_config_file:
return
def tearDown(self): self.hass = get_test_home_assistant()
self.backup_cache = loader._COMPONENT_CACHE
def teardown_method(self, method):
"""Clean up.""" """Clean up."""
dt_util.DEFAULT_TIME_ZONE = self.orig_timezone dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE
if method == self.test_from_config_file:
return
self.hass.stop()
loader._COMPONENT_CACHE = self.backup_cache
def test_from_config_file(self): def test_from_config_file(self):
"""Test with configuration file.""" """Test with configuration file."""
@ -36,8 +52,7 @@ class TestBootstrap(unittest.TestCase):
components.append('group') components.append('group')
self.assertEqual(sorted(components), assert sorted(components) == sorted(hass.config.components)
sorted(hass.config.components))
def test_remove_lib_on_upgrade(self): def test_remove_lib_on_upgrade(self):
"""Test removal of library on upgrade.""" """Test removal of library on upgrade."""
@ -54,13 +69,11 @@ class TestBootstrap(unittest.TestCase):
with open(check_file, 'w'): with open(check_file, 'w'):
pass pass
hass = get_test_home_assistant() self.hass.config.config_dir = config_dir
hass.config.config_dir = config_dir
self.assertTrue(os.path.isfile(check_file)) assert os.path.isfile(check_file)
bootstrap.process_ha_config_upgrade(hass) bootstrap.process_ha_config_upgrade(self.hass)
self.assertFalse(os.path.isfile(check_file)) assert not os.path.isfile(check_file)
hass.stop()
def test_not_remove_lib_if_not_upgrade(self): def test_not_remove_lib_if_not_upgrade(self):
"""Test removal of library with no upgrade.""" """Test removal of library with no upgrade."""
@ -77,13 +90,11 @@ class TestBootstrap(unittest.TestCase):
with open(check_file, 'w'): with open(check_file, 'w'):
pass pass
hass = get_test_home_assistant() self.hass.config.config_dir = config_dir
hass.config.config_dir = config_dir
bootstrap.process_ha_config_upgrade(hass) bootstrap.process_ha_config_upgrade(self.hass)
self.assertTrue(os.path.isfile(check_file)) assert os.path.isfile(check_file)
hass.stop()
def test_entity_customization(self): def test_entity_customization(self):
"""Test entity customization through configuration.""" """Test entity customization through configuration."""
@ -92,23 +103,19 @@ class TestBootstrap(unittest.TestCase):
CONF_NAME: 'Test', CONF_NAME: 'Test',
CONF_CUSTOMIZE: {'test.test': {'hidden': True}}} CONF_CUSTOMIZE: {'test.test': {'hidden': True}}}
hass = get_test_home_assistant() bootstrap.process_ha_core_config(self.hass, config)
bootstrap.process_ha_core_config(hass, config)
entity = Entity() entity = Entity()
entity.entity_id = 'test.test' entity.entity_id = 'test.test'
entity.hass = hass entity.hass = self.hass
entity.update_ha_state() entity.update_ha_state()
state = hass.states.get('test.test') state = self.hass.states.get('test.test')
self.assertTrue(state.attributes['hidden']) assert state.attributes['hidden']
hass.stop()
def test_handle_setup_circular_dependency(self): def test_handle_setup_circular_dependency(self):
"""Test the setup of circular dependencies.""" """Test the setup of circular dependencies."""
hass = get_test_home_assistant()
loader.set_component('comp_b', MockModule('comp_b', ['comp_a'])) loader.set_component('comp_b', MockModule('comp_b', ['comp_a']))
def setup_a(hass, config): def setup_a(hass, config):
@ -118,6 +125,180 @@ class TestBootstrap(unittest.TestCase):
loader.set_component('comp_a', MockModule('comp_a', setup=setup_a)) loader.set_component('comp_a', MockModule('comp_a', setup=setup_a))
bootstrap.setup_component(hass, 'comp_a') bootstrap.setup_component(self.hass, 'comp_a')
self.assertEqual(['comp_a'], hass.config.components) assert ['comp_a'] == self.hass.config.components
hass.stop()
def test_validate_component_config(self):
"""Test validating component configuration."""
config_schema = vol.Schema({
'comp_conf': {
'hello': str
}
}, required=True)
loader.set_component(
'comp_conf', MockModule('comp_conf', config_schema=config_schema))
assert not bootstrap._setup_component(self.hass, 'comp_conf', {})
assert not bootstrap._setup_component(self.hass, 'comp_conf', {
'comp_conf': None
})
assert not bootstrap._setup_component(self.hass, 'comp_conf', {
'comp_conf': {}
})
assert not bootstrap._setup_component(self.hass, 'comp_conf', {
'comp_conf': {
'hello': 'world',
'invalid': 'extra',
}
})
assert bootstrap._setup_component(self.hass, 'comp_conf', {
'comp_conf': {
'hello': 'world',
}
})
def test_validate_platform_config(self):
"""Test validating platform configuration."""
platform_schema = PLATFORM_SCHEMA.extend({
'hello': str,
}, required=True)
loader.set_component(
'platform_conf',
MockModule('platform_conf', platform_schema=platform_schema))
assert not bootstrap._setup_component(self.hass, 'platform_conf', {
'platform_conf': None
})
assert not bootstrap._setup_component(self.hass, 'platform_conf', {
'platform_conf': {}
})
assert not bootstrap._setup_component(self.hass, 'platform_conf', {
'platform_conf': {
'hello': 'world',
'invalid': 'extra',
}
})
assert not bootstrap._setup_component(self.hass, 'platform_conf', {
'platform_conf': {
'platform': 'whatever',
'hello': 'world',
},
'platform_conf 2': {
'invalid': True
}
})
assert bootstrap._setup_component(self.hass, 'platform_conf', {
'platform_conf': {
'platform': 'whatever',
'hello': 'world',
}
})
assert bootstrap._setup_component(self.hass, 'platform_conf', {
'platform_conf': [{
'platform': 'whatever',
'hello': 'world',
}]
})
def test_component_not_found(self):
"""setup_component should not crash if component doesn't exist."""
assert not bootstrap.setup_component(self.hass, 'non_existing')
def test_component_not_double_initialized(self):
"""Test we do not setup a component twice."""
mock_setup = mock.MagicMock()
loader.set_component('comp', MockModule('comp', setup=mock_setup))
assert bootstrap.setup_component(self.hass, 'comp')
assert mock_setup.called
mock_setup.reset_mock()
assert bootstrap.setup_component(self.hass, 'comp')
assert not mock_setup.called
@mock.patch('homeassistant.util.package.install_package',
return_value=False)
def test_component_not_installed_if_requirement_fails(self, mock_install):
"""Component setup should fail if requirement can't install."""
loader.set_component(
'comp', MockModule('comp', requirements=['package==0.0.1']))
assert not bootstrap.setup_component(self.hass, 'comp')
assert 'comp' not in self.hass.config.components
def test_component_not_setup_twice_if_loaded_during_other_setup(self):
"""
Test component that gets setup while waiting for lock is not setup
twice.
"""
loader.set_component('comp', MockModule('comp'))
result = []
def setup_component():
result.append(bootstrap.setup_component(self.hass, 'comp'))
with bootstrap._SETUP_LOCK:
thread = threading.Thread(target=setup_component)
thread.start()
self.hass.config.components.append('comp')
thread.join()
assert len(result) == 1
assert result[0]
def test_component_not_setup_missing_dependencies(self):
"""Test we do not setup a component if not all dependencies loaded."""
deps = ['non_existing']
loader.set_component('comp', MockModule('comp', dependencies=deps))
assert not bootstrap._setup_component(self.hass, 'comp', None)
assert 'comp' not in self.hass.config.components
self.hass.config.components.append('non_existing')
assert bootstrap._setup_component(self.hass, 'comp', None)
def test_component_failing_setup(self):
"""Test component that fails setup."""
loader.set_component(
'comp', MockModule('comp', setup=lambda hass, config: False))
assert not bootstrap._setup_component(self.hass, 'comp', None)
assert 'comp' not in self.hass.config.components
def test_component_exception_setup(self):
"""Test component that raises exception during setup."""
def exception_setup(hass, config):
"""Setup that raises exception."""
raise Exception('fail!')
loader.set_component('comp', MockModule('comp', setup=exception_setup))
assert not bootstrap._setup_component(self.hass, 'comp', None)
assert 'comp' not in self.hass.config.components
@mock.patch('homeassistant.bootstrap.process_ha_core_config')
def test_home_assistant_core_config_validation(self, mock_process):
"""Test if we pass in wrong information for HA conf."""
# Extensive HA conf validation testing is done in test_config.py
assert None is bootstrap.from_config_dict({
'homeassistant': {
'latitude': 'some string'
}
})
assert not mock_process.called

View File

@ -4,6 +4,9 @@ import unittest
import unittest.mock as mock import unittest.mock as mock
import os import os
import pytest
from voluptuous import MultipleInvalid
from homeassistant.core import DOMAIN, HomeAssistantError from homeassistant.core import DOMAIN, HomeAssistantError
import homeassistant.config as config_util import homeassistant.config as config_util
from homeassistant.const import ( from homeassistant.const import (
@ -138,3 +141,28 @@ class TestConfig(unittest.TestCase):
config_util.create_default_config( config_util.create_default_config(
os.path.join(CONFIG_DIR, 'non_existing_dir/'), False)) os.path.join(CONFIG_DIR, 'non_existing_dir/'), False))
self.assertTrue(mock_print.called) 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,
},
},
})