diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 9b003798..0ab50803 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -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.""" diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index ddff3537..9006841c 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -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 diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 5f03f698..63b16939 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -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 + } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 60b3e34a..3fc771cc 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -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)