Add initial config validation
This commit is contained in:
parent
0549bc0290
commit
5baa98b79f
24 changed files with 349 additions and 72 deletions
|
@ -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."""
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
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):
|
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)
|
||||||
|
@ -336,40 +364,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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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" \
|
||||||
|
|
|
@ -29,30 +29,18 @@ 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)
|
yield item.get(CONF_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):
|
||||||
|
|
65
homeassistant/helpers/config_validation.py
Normal file
65
homeassistant/helpers/config_validation.py
Normal 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')
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -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(
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
114
tests/helpers/test_config_validation.py
Normal file
114
tests/helpers/test_config_validation.py
Normal 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')
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue