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,
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."""

View File

@ -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
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):
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)
@ -262,8 +290,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
}
))
except ImportError:
_LOGGER.warning(
"Colorlog package not found, console coloring disabled")
pass
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
@ -336,40 +363,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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View File

@ -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,

View File

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

View File

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

View File

@ -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,

View File

@ -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" \

View File

@ -29,30 +29,19 @@ 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)
platform = None if item is None else item.get(CONF_PLATFORM)
yield platform, item
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
# 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:

View File

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

View File

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

View File

@ -143,10 +143,19 @@ class MockHTTP(object):
class MockModule(object):
"""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."""
self.DOMAIN = domain
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.
if setup is None:
self.setup = lambda hass, config: True

View File

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

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
import os
import tempfile
import unittest
from unittest import mock
import threading
import voluptuous as vol
from homeassistant import bootstrap, loader
from homeassistant.const import (__version__, CONF_LATITUDE, CONF_LONGITUDE,
CONF_NAME, CONF_CUSTOMIZE)
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
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."""
def setUp(self):
def setup_method(self, method):
"""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."""
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):
"""Test with configuration file."""
@ -36,8 +52,7 @@ class TestBootstrap(unittest.TestCase):
components.append('group')
self.assertEqual(sorted(components),
sorted(hass.config.components))
assert sorted(components) == sorted(hass.config.components)
def test_remove_lib_on_upgrade(self):
"""Test removal of library on upgrade."""
@ -54,13 +69,11 @@ class TestBootstrap(unittest.TestCase):
with open(check_file, 'w'):
pass
hass = get_test_home_assistant()
hass.config.config_dir = config_dir
self.hass.config.config_dir = config_dir
self.assertTrue(os.path.isfile(check_file))
bootstrap.process_ha_config_upgrade(hass)
self.assertFalse(os.path.isfile(check_file))
hass.stop()
assert os.path.isfile(check_file)
bootstrap.process_ha_config_upgrade(self.hass)
assert not os.path.isfile(check_file)
def test_not_remove_lib_if_not_upgrade(self):
"""Test removal of library with no upgrade."""
@ -77,13 +90,11 @@ class TestBootstrap(unittest.TestCase):
with open(check_file, 'w'):
pass
hass = get_test_home_assistant()
hass.config.config_dir = config_dir
self.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))
hass.stop()
assert os.path.isfile(check_file)
def test_entity_customization(self):
"""Test entity customization through configuration."""
@ -92,23 +103,19 @@ class TestBootstrap(unittest.TestCase):
CONF_NAME: 'Test',
CONF_CUSTOMIZE: {'test.test': {'hidden': True}}}
hass = get_test_home_assistant()
bootstrap.process_ha_core_config(hass, config)
bootstrap.process_ha_core_config(self.hass, config)
entity = Entity()
entity.entity_id = 'test.test'
entity.hass = hass
entity.hass = self.hass
entity.update_ha_state()
state = hass.states.get('test.test')
state = self.hass.states.get('test.test')
self.assertTrue(state.attributes['hidden'])
hass.stop()
assert state.attributes['hidden']
def test_handle_setup_circular_dependency(self):
"""Test the setup of circular dependencies."""
hass = get_test_home_assistant()
loader.set_component('comp_b', MockModule('comp_b', ['comp_a']))
def setup_a(hass, config):
@ -118,6 +125,180 @@ class TestBootstrap(unittest.TestCase):
loader.set_component('comp_a', MockModule('comp_a', setup=setup_a))
bootstrap.setup_component(hass, 'comp_a')
self.assertEqual(['comp_a'], hass.config.components)
hass.stop()
bootstrap.setup_component(self.hass, 'comp_a')
assert ['comp_a'] == self.hass.config.components
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 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,
},
},
})