start with YAML config validation

This commit is contained in:
Bas Nijholt 2020-09-23 18:20:25 +02:00
commit 3b275e1dd3
4 changed files with 112 additions and 90 deletions

View file

@ -29,15 +29,36 @@ Technical notes: I had to make a lot of assumptions when writing this app
import asyncio
import logging
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from .const import DOMAIN, UNDO_UPDATE_LISTENER
from .const import CONF_NAME, DOMAIN, UNDO_UPDATE_LISTENER, get_domain_schema
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["switch"]
def _all_unique_profiles(value):
"""Validate that all enties have a unique profile name."""
hosts = [device[CONF_NAME] for device in value]
schema = vol.Schema(vol.Unique())
schema(hosts)
return value
_DOMAIN_SCHEMA = get_domain_schema(with_fake_none=False)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.ensure_list, [vol.Schema(_DOMAIN_SCHEMA)], _all_unique_profiles
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Import integration from config."""

View file

@ -1,51 +1,18 @@
"""Config flow for Coronavirus integration."""
import logging
import voluptuous as vol
from copy import copy
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.light import VALID_TRANSITION
from homeassistant.core import callback
from .const import (
CONF_DISABLE_BRIGHTNESS_ADJUST,
CONF_DISABLE_ENTITY,
CONF_DISABLE_STATE,
CONF_INITIAL_TRANSITION,
CONF_INTERVAL,
CONF_LIGHTS,
CONF_MAX_BRIGHTNESS,
CONF_MAX_COLOR_TEMP,
CONF_MIN_BRIGHTNESS,
CONF_MIN_COLOR_TEMP,
CONF_ONLY_ONCE,
CONF_SLEEP_BRIGHTNESS,
CONF_SLEEP_COLOR_TEMP,
CONF_SLEEP_ENTITY,
CONF_SLEEP_STATE,
CONF_SUNRISE_OFFSET,
CONF_SUNRISE_TIME,
CONF_SUNSET_OFFSET,
CONF_SUNSET_TIME,
CONF_TRANSITION,
DEFAULT_DISABLE_BRIGHTNESS_ADJUST,
DEFAULT_INITIAL_TRANSITION,
DEFAULT_INTERVAL,
DEFAULT_LIGHTS,
DEFAULT_MAX_BRIGHTNESS,
DEFAULT_MAX_COLOR_TEMP,
DEFAULT_MIN_BRIGHTNESS,
DEFAULT_MIN_COLOR_TEMP,
DEFAULT_ONLY_ONCE,
DEFAULT_SLEEP_BRIGHTNESS,
DEFAULT_SLEEP_COLOR_TEMP,
DEFAULT_SUNRISE_OFFSET,
DEFAULT_SUNSET_OFFSET,
DEFAULT_TRANSITION,
DOMAIN,
EXTRA_VALIDATION,
FAKE_NONE,
VALIDATION_TUPLES,
get_domain_schema,
)
_LOGGER = logging.getLogger(__name__)
@ -71,6 +38,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_import(self, user_input=None):
"""Handle configuration by yaml file."""
_DOMAIN_SCHEMA = get_domain_schema(with_fake_none=True)
schema = {k: v for k, v in _DOMAIN_SCHEMA.items() if k in user_input}
vol.Schema(schema)(user_input)
_LOGGER.error(str(user_input) + str(schema))
return self.async_create_entry(title="", data=user_input)
@staticmethod
@callback
def async_get_options_flow(config_entry):
@ -78,6 +53,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return OptionsFlowHandler(config_entry)
def validate_options(user_input, errors):
for key, validate in EXTRA_VALIDATION.items():
# these are unserializable validators
try:
value = user_input.get(key)
if value is not None and value != FAKE_NONE:
validate(user_input[key])
except vol.Invalid:
_LOGGER.exception("Configuration option %s=%s is incorrect", key, value)
errors["base"] = "option_error"
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow for Adaptive Lighting."""
@ -89,46 +76,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow."""
errors = {}
if user_input is not None:
for key, validate in EXTRA_VALIDATION:
# these are unserializable validators
try:
value = user_input.get(key)
if value is not None and value != FAKE_NONE:
validate(user_input[key])
except vol.Invalid:
_LOGGER.exception(
"Configuration option %s=%s is incorrect", key, value
)
errors["base"] = "option_error"
validate_options(user_input, errors)
if not errors:
return self.async_create_entry(title="", data=user_input)
options = self.config_entry.options
int_between = lambda a, b: vol.All(vol.Coerce(int), vol.Range(min=a, max=b))
all_lights = cv.multi_select(self.hass.states.async_entity_ids("light"))
validation_tuples = [
(CONF_LIGHTS, DEFAULT_LIGHTS, all_lights),
(CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool),
(CONF_DISABLE_ENTITY, FAKE_NONE, str),
(CONF_DISABLE_STATE, FAKE_NONE, str),
(CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION),
(CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int),
(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)),
(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)),
(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)),
(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)),
(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)),
(CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)),
(CONF_SLEEP_ENTITY, FAKE_NONE, str),
(CONF_SLEEP_STATE, FAKE_NONE, str),
(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int),
(CONF_SUNRISE_TIME, FAKE_NONE, str),
(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int),
(CONF_SUNSET_TIME, FAKE_NONE, str),
(CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION),
]
validation_tuples = copy(VALIDATION_TUPLES)
lights_tuple = (*validation_tuples[0][:-1], all_lights)
validation_tuples[0] = lights_tuple
options = self.config_entry.options
options_schema = vol.Schema(
{
vol.Optional(key, default=options.get(key, default)): validation

View file

@ -1,6 +1,6 @@
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.components.light import VALID_TRANSITION
ICON = "mdi:theme-light-dark"
@ -36,14 +36,60 @@ CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60
UNDO_UPDATE_LISTENER = "undo_update_listener"
FAKE_NONE = "None" # TODO: use `from homeassistant.const import ENTITY_MATCH_NONE`?
EXTRA_VALIDATION = [ # these validators cannot be serialized
(CONF_DISABLE_ENTITY, cv.entity_id),
(CONF_DISABLE_STATE, vol.All(cv.ensure_list_csv, [cv.string])),
(CONF_INTERVAL, cv.time_period),
(CONF_SLEEP_ENTITY, cv.entity_id),
(CONF_SLEEP_STATE, vol.All(cv.ensure_list_csv, [cv.string])),
(CONF_SUNRISE_OFFSET, cv.time_period),
(CONF_SUNRISE_TIME, cv.time),
(CONF_SUNSET_OFFSET, cv.time_period),
(CONF_SUNSET_TIME, cv.time),
def int_between(a, b):
return vol.All(vol.Coerce(int), vol.Range(min=a, max=b))
VALIDATION_TUPLES = [
(CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids),
(CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool),
(CONF_DISABLE_ENTITY, FAKE_NONE, str),
(CONF_DISABLE_STATE, FAKE_NONE, str),
(CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION),
(CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int),
(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)),
(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)),
(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)),
(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)),
(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)),
(CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)),
(CONF_SLEEP_ENTITY, FAKE_NONE, str),
(CONF_SLEEP_STATE, FAKE_NONE, str),
(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int),
(CONF_SUNRISE_TIME, FAKE_NONE, str),
(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int),
(CONF_SUNSET_TIME, FAKE_NONE, str),
(CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION),
]
EXTRA_VALIDATION = { # these validators cannot be serialized
CONF_DISABLE_ENTITY: cv.entity_id,
CONF_DISABLE_STATE: vol.All(cv.ensure_list_csv, [cv.string]),
CONF_INTERVAL: cv.time_period,
CONF_SLEEP_ENTITY: cv.entity_id,
CONF_SLEEP_STATE: vol.All(cv.ensure_list_csv, [cv.string]),
CONF_SUNRISE_OFFSET: cv.time_period,
CONF_SUNRISE_TIME: cv.time,
CONF_SUNSET_OFFSET: cv.time_period,
CONF_SUNSET_TIME: cv.time,
}
def get_domain_schema(with_fake_none=False):
validation_tuples = [
(key, default, EXTRA_VALIDATION.get(key, validation))
for key, default, validation in VALIDATION_TUPLES
]
validation_tuples.append((CONF_NAME, DEFAULT_NAME, cv.string))
def replace_none(x):
if not with_fake_none and x == FAKE_NONE:
return vol.UNDEFINED
return x
return {
vol.Optional(key, default=replace_none(default)): validation
for key, default, validation in validation_tuples
}

View file

@ -1,10 +1,8 @@
# CHECK OUT THE VIZIO COMPONENT!
"""Adaptive Lighting Component for Home-Assistant."""
import asyncio
import bisect
import logging
from copy import deepcopy
from datetime import timedelta
import homeassistant.util.dt as dt_util
@ -128,11 +126,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._entity_id = f"switch.{DOMAIN}_{slugify(name)}"
self._icon = ICON
data = {**config_entry.options, **config_entry.data}
opts = { # replace "None" -> None
key: value if value != FAKE_NONE else None
for key, value in config_entry.options.items()
key: value if value != FAKE_NONE else None for key, value in data.items()
}
for key, validate in EXTRA_VALIDATION: # Fix the types of the inputs
for key, validate in EXTRA_VALIDATION.items(): # Fix the types of the inputs
value = opts.get(key)
if value is not None:
opts[key] = validate(value)