2016-03-28 01:48:51 +00:00
|
|
|
"""Helpers for config validation using voluptuous."""
|
2016-04-04 19:18:58 +00:00
|
|
|
from datetime import timedelta
|
|
|
|
|
2016-04-03 17:19:09 +00:00
|
|
|
import jinja2
|
2016-03-28 01:48:51 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2016-04-04 19:18:58 +00:00
|
|
|
from homeassistant.loader import get_platform
|
2016-03-28 01:48:51 +00:00
|
|
|
from homeassistant.const import (
|
2016-04-03 03:10:57 +00:00
|
|
|
CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELCIUS, TEMP_FAHRENHEIT)
|
2016-03-28 01:48:51 +00:00
|
|
|
from homeassistant.helpers.entity import valid_entity_id
|
|
|
|
import homeassistant.util.dt as dt_util
|
2016-04-03 17:19:09 +00:00
|
|
|
from homeassistant.util import slugify
|
2016-03-28 01:48:51 +00:00
|
|
|
|
|
|
|
# pylint: disable=invalid-name
|
|
|
|
|
2016-04-03 17:19:09 +00:00
|
|
|
# Home Assistant types
|
2016-04-01 03:19:59 +00:00
|
|
|
byte = vol.All(vol.Coerce(int), vol.Range(min=0, max=255))
|
|
|
|
small_float = vol.All(vol.Coerce(float), vol.Range(min=0, max=1))
|
2016-04-03 17:19:09 +00:00
|
|
|
latitude = vol.All(vol.Coerce(float), vol.Range(min=-90, max=90),
|
|
|
|
msg='invalid latitude')
|
|
|
|
longitude = vol.All(vol.Coerce(float), vol.Range(min=-180, max=180),
|
|
|
|
msg='invalid longitude')
|
|
|
|
|
|
|
|
|
|
|
|
def boolean(value):
|
|
|
|
"""Validate and coerce a boolean value."""
|
|
|
|
if isinstance(value, str):
|
2016-04-04 04:38:58 +00:00
|
|
|
value = value.lower()
|
2016-04-03 17:19:09 +00:00
|
|
|
if value in ('1', 'true', 'yes', 'on', 'enable'):
|
|
|
|
return True
|
|
|
|
if value in ('0', 'false', 'no', 'off', 'disable'):
|
|
|
|
return False
|
|
|
|
raise vol.Invalid('invalid boolean value {}'.format(value))
|
|
|
|
return bool(value)
|
2016-03-28 01:48:51 +00:00
|
|
|
|
|
|
|
|
2016-04-07 17:52:25 +00:00
|
|
|
def isfile(value):
|
|
|
|
"""Validate that the value is an existing file."""
|
|
|
|
return vol.IsFile('not a file')(value)
|
|
|
|
|
|
|
|
|
2016-04-04 19:18:58 +00:00
|
|
|
def ensure_list(value):
|
|
|
|
"""Wrap value in list if it is not one."""
|
|
|
|
return value if isinstance(value, list) else [value]
|
|
|
|
|
|
|
|
|
2016-03-28 01:48:51 +00:00
|
|
|
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:"')
|
|
|
|
|
|
|
|
|
2016-04-04 19:18:58 +00:00
|
|
|
def time_offset(value):
|
|
|
|
"""Validate and transform time offset."""
|
|
|
|
if not isinstance(value, str):
|
|
|
|
raise vol.Invalid('offset should be a string')
|
|
|
|
|
|
|
|
negative_offset = False
|
|
|
|
if value.startswith('-'):
|
|
|
|
negative_offset = True
|
|
|
|
value = value[1:]
|
|
|
|
elif value.startswith('+'):
|
|
|
|
value = value[1:]
|
|
|
|
|
|
|
|
try:
|
|
|
|
parsed = [int(x) for x in value.split(':')]
|
|
|
|
except ValueError:
|
|
|
|
raise vol.Invalid(
|
|
|
|
'offset {} should be format HH:MM or HH:MM:SS'.format(value))
|
|
|
|
|
|
|
|
if len(parsed) == 2:
|
|
|
|
hour, minute = parsed
|
|
|
|
second = 0
|
|
|
|
elif len(parsed) == 3:
|
|
|
|
hour, minute, second = parsed
|
|
|
|
else:
|
|
|
|
raise vol.Invalid(
|
|
|
|
'offset {} should be format HH:MM or HH:MM:SS'.format(value))
|
|
|
|
|
|
|
|
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
|
|
|
|
|
|
|
if negative_offset:
|
|
|
|
offset *= -1
|
|
|
|
|
|
|
|
return offset
|
|
|
|
|
|
|
|
|
|
|
|
def match_all(value):
|
|
|
|
"""Validator that matches all values."""
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def platform_validator(domain):
|
|
|
|
"""Validate if platform exists for given domain."""
|
|
|
|
def validator(value):
|
|
|
|
"""Test if platform exists."""
|
|
|
|
if value is None:
|
|
|
|
raise vol.Invalid('platform cannot be None')
|
|
|
|
if get_platform(domain, str(value)):
|
|
|
|
return value
|
|
|
|
raise vol.Invalid(
|
|
|
|
'platform {} does not exist for {}'.format(value, domain))
|
|
|
|
return validator
|
|
|
|
|
|
|
|
|
2016-04-03 17:19:09 +00:00
|
|
|
def service(value):
|
|
|
|
"""Validate service."""
|
|
|
|
# Services use same format as entities so we can use same helper.
|
|
|
|
if valid_entity_id(value):
|
|
|
|
return value
|
|
|
|
raise vol.Invalid('Service {} does not match format <domain>.<name>'
|
|
|
|
.format(value))
|
|
|
|
|
|
|
|
|
|
|
|
def slug(value):
|
|
|
|
"""Validate value is a valid slug."""
|
|
|
|
if value is None:
|
|
|
|
raise vol.Invalid('Slug should not be None')
|
|
|
|
value = str(value)
|
|
|
|
slg = slugify(value)
|
|
|
|
if value == slg:
|
|
|
|
return value
|
|
|
|
raise vol.Invalid('invalid slug {} (try {})'.format(value, slg))
|
|
|
|
|
|
|
|
|
2016-04-02 07:51:03 +00:00
|
|
|
def string(value):
|
|
|
|
"""Coerce value to string, except for None."""
|
|
|
|
if value is not None:
|
|
|
|
return str(value)
|
2016-04-03 17:19:09 +00:00
|
|
|
raise vol.Invalid('string value is None')
|
2016-04-02 07:51:03 +00:00
|
|
|
|
|
|
|
|
2016-03-28 01:48:51 +00:00
|
|
|
def temperature_unit(value):
|
|
|
|
"""Validate and transform temperature unit."""
|
2016-04-03 17:19:09 +00:00
|
|
|
value = str(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 template(value):
|
|
|
|
"""Validate a jinja2 template."""
|
|
|
|
if value is None:
|
|
|
|
raise vol.Invalid('template value is None')
|
|
|
|
|
|
|
|
value = str(value)
|
|
|
|
try:
|
|
|
|
jinja2.Environment().parse(value)
|
|
|
|
return value
|
|
|
|
except jinja2.exceptions.TemplateSyntaxError as ex:
|
|
|
|
raise vol.Invalid('invalid template ({})'.format(ex))
|
2016-03-28 01:48:51 +00:00
|
|
|
|
|
|
|
|
|
|
|
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')
|
2016-04-03 17:19:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
# Validator helpers
|
|
|
|
|
2016-04-04 19:18:58 +00:00
|
|
|
def key_dependency(key, dependency):
|
|
|
|
"""Validate that all dependencies exist for key."""
|
|
|
|
def validator(value):
|
|
|
|
"""Test dependencies."""
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
raise vol.Invalid('key dependencies require a dict')
|
|
|
|
print(key, value)
|
|
|
|
if key in value and dependency not in value:
|
|
|
|
raise vol.Invalid('dependency violation - key "{}" requires '
|
|
|
|
'key "{}" to exist'.format(key, dependency))
|
2016-04-03 17:19:09 +00:00
|
|
|
|
2016-04-04 19:18:58 +00:00
|
|
|
return value
|
|
|
|
return validator
|
2016-04-03 17:19:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
# Adapted from:
|
|
|
|
# https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666
|
2016-04-04 19:18:58 +00:00
|
|
|
def has_at_least_one_key(*keys):
|
2016-04-03 17:19:09 +00:00
|
|
|
"""Validator that at least one key exists."""
|
|
|
|
def validate(obj):
|
|
|
|
"""Test keys exist in dict."""
|
|
|
|
if not isinstance(obj, dict):
|
|
|
|
raise vol.Invalid('expected dictionary')
|
|
|
|
|
|
|
|
for k in obj.keys():
|
|
|
|
if k in keys:
|
|
|
|
return obj
|
|
|
|
raise vol.Invalid('must contain one of {}.'.format(', '.join(keys)))
|
|
|
|
|
|
|
|
return validate
|
|
|
|
|
|
|
|
|
|
|
|
# Schemas
|
|
|
|
|
|
|
|
PLATFORM_SCHEMA = vol.Schema({
|
|
|
|
vol.Required(CONF_PLATFORM): string,
|
|
|
|
CONF_SCAN_INTERVAL: vol.All(vol.Coerce(int), vol.Range(min=1)),
|
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
EVENT_SCHEMA = vol.Schema({
|
|
|
|
vol.Required('event'): string,
|
|
|
|
'event_data': dict
|
|
|
|
})
|
|
|
|
|
|
|
|
SERVICE_SCHEMA = vol.All(vol.Schema({
|
|
|
|
vol.Exclusive('service', 'service name'): service,
|
|
|
|
vol.Exclusive('service_template', 'service name'): string,
|
|
|
|
vol.Exclusive('data', 'service data'): dict,
|
2016-04-04 19:18:58 +00:00
|
|
|
vol.Exclusive('data_template', 'service data'): {match_all: template},
|
|
|
|
'entity_id': entity_ids,
|
|
|
|
}), has_at_least_one_key('service', 'service_template'))
|