diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index ca7f15ac..dd3988e9 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -1 +1,206 @@ -"""Adaptive Lighting Component for Home-Assistant.""" +""" +Adaptive Lighting Component for Home-Assistant. + +This component calculates color temperature and brightness to synchronize +your color changing lights with perceived color temperature of the sky throughout +the day. This gives your environment a more natural feel, with cooler whites during +the midday and warmer tints near twilight and dawn. + +In addition, the component sets your lights to a nice warm white at 1% in "Sleep" mode, +which is far brighter than starlight but won't reset your adaptive rhythm or break down +too much rhodopsin in your eyes. + +Human circadian rhythms are heavily influenced by ambient light levels and +hues. Hormone production, brainwave activity, mood and wakefulness are +just some of the cognitive functions tied to cyclical natural light. +http://en.wikipedia.org/wiki/Zeitgeber + +Here's some further reading: + +http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm +http://en.wikipedia.org/wiki/Color_temperature + +Technical notes: I had to make a lot of assumptions when writing this app +* There are no considerations for weather or altitude, but does use your + hub's location to calculate the sun position. +* The component doesn't calculate a true "Blue Hour" -- it just sets the + lights to 2700K (warm white) until your hub goes into Night mode +""" +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.light import ( + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, + ATTR_TRANSITION, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import ( + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, + VALID_TRANSITION, + is_on, +) +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_NAME, + SERVICE_TURN_ON, + STATE_ON, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import ( + async_track_state_change, + async_track_time_interval, +) +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.sun import get_astral_location +from homeassistant.util import slugify +from homeassistant.util.color import ( + color_RGB_to_xy, + color_temperature_kelvin_to_mired, + color_temperature_to_rgb, + color_xy_to_hs, +) + +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_NAME, + DEFAULT_NAME, + 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, + ICON, + SUN_EVENT_MIDNIGHT, + SUN_EVENT_NOON, +) + + +_SUPPORT_OPTS = { + "brightness": SUPPORT_BRIGHTNESS, + "color_temp": SUPPORT_COLOR_TEMP, + "color": SUPPORT_COLOR, + "transition": SUPPORT_TRANSITION, +} + + +_LOGGER = logging.getLogger(__name__) + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, + vol.Optional( + CONF_DISABLE_BRIGHTNESS_ADJUST, + default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST, + ): cv.boolean, + vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, + vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional( + CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION + ): VALID_TRANSITION, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, + vol.Optional( + CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional( + CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP + ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional( + CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional( + CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP + ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, + vol.Optional( + CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional( + CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP + ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, + vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional( + CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET + ): cv.time_period, + vol.Optional(CONF_SUNRISE_TIME): cv.time, + vol.Optional( + CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET + ): cv.time_period, + vol.Optional(CONF_SUNSET_TIME): cv.time, + vol.Optional( + CONF_TRANSITION, default=DEFAULT_TRANSITION + ): VALID_TRANSITION, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Import integration from config.""" + + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the component.""" + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "switch") + ) + + return True diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index af59f121..5785c0b8 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -5,9 +5,46 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant import config_entries +from homeassistant.components.light import VALID_TRANSITION from homeassistant.core import callback -from .const import DOMAIN, _convert_to_options_schema +from .const import ( # _convert_to_options_schema, + 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, +) _LOGGER = logging.getLogger(__name__) @@ -51,8 +88,79 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - options_schema = _convert_to_options_schema( - self.hass, self.config_entry.options + options = self.config_entry.options + + lights = options.get(CONF_LIGHTS, DEFAULT_LIGHTS) + disable_brightness_adjust = options.get( + CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST + ) + disable_entity = options.get(CONF_DISABLE_ENTITY) + disable_state = options.get(CONF_DISABLE_STATE) + initial_transition = options.get( + CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION + ) + interval = options.get(CONF_INTERVAL, DEFAULT_INTERVAL) + max_brightness = options.get(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS) + max_color_temp = options.get(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP) + min_brightness = options.get(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS) + min_color_temp = options.get(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP) + only_once = options.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) + sleep_brightness = options.get(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS) + sleep_color_temp = options.get(CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP) + sleep_entity = options.get(CONF_SLEEP_ENTITY) + sleep_state = options.get(CONF_SLEEP_STATE) + sunrise_offset = options.get(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET) + sunrise_time = options.get(CONF_SUNRISE_TIME) + sunset_offset = options.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) + sunset_time = options.get(CONF_SUNSET_TIME) + transition = options.get(CONF_TRANSITION, DEFAULT_TRANSITION) + + all_lights = self.hass.states.async_entity_ids("light") + all_lights = cv.multi_select(all_lights) + + options_schema = vol.Schema( + { + vol.Optional(CONF_LIGHTS, default=lights): all_lights, + # vol.Optional( + # CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust + # ): bool, + # vol.Optional(CONF_DISABLE_ENTITY, default=disable_entity): str, + # vol.Optional(CONF_DISABLE_STATE, default=disable_state): str, + # vol.Optional( + # CONF_INITIAL_TRANSITION, default=initial_transition + # ): cv.positive_int, + # vol.Optional(CONF_INTERVAL, default=interval): cv.positive_int, + # vol.Optional(CONF_MAX_BRIGHTNESS, default=max_brightness): vol.All( + # vol.Coerce(int), vol.Range(min=1, max=100) + # ), + # vol.Optional(CONF_MAX_COLOR_TEMP, default=max_color_temp): vol.All( + # vol.Coerce(int), vol.Range(min=1000, max=10000) + # ), + # vol.Optional(CONF_MIN_BRIGHTNESS, default=min_brightness): vol.All( + # vol.Coerce(int), vol.Range(min=1, max=100) + # ), + # vol.Optional(CONF_MIN_COLOR_TEMP, default=min_color_temp): vol.All( + # vol.Coerce(int), vol.Range(min=1000, max=10000) + # ), + # vol.Optional(CONF_ONLY_ONCE, default=only_once): bool, + # vol.Optional(CONF_SLEEP_BRIGHTNESS, default=sleep_brightness): vol.All( + # vol.Coerce(int), vol.Range(min=1, max=100) + # ), + # vol.Optional(CONF_SLEEP_COLOR_TEMP, default=sleep_color_temp): vol.All( + # vol.Coerce(int), vol.Range(min=1000, max=10000) + # ), + # vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, + # vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, + # vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, + # vol.Optional(CONF_SUNRISE_TIME, default=sunrise_time): str, + # vol.Optional(CONF_SUNSET_OFFSET, default=sunset_offset): int, + # vol.Optional(CONF_SUNSET_TIME, default=sunset_time): str, + # vol.Optional(CONF_TRANSITION, default=transition): VALID_TRANSITION, + } ) + # options_schema = _convert_to_options_schema( + # self.hass, self.config_entry.options + # ) + return self.async_show_form(step_id="init", data_schema=options_schema) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 5345b777..cdb3154d 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -9,6 +9,7 @@ DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" +CONF_NAME, DEFAULT_NAME = "name", "default" CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST = ( "disable_brightness_adjust", @@ -36,34 +37,18 @@ CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 _COMMON_SCHEMA = { vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, - vol.Optional( - CONF_DISABLE_BRIGHTNESS_ADJUST, default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST - ): cv.boolean, + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST): cv.boolean, vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional( - CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION - ): VALID_TRANSITION, + vol.Optional(CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION): VALID_TRANSITION, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), + vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, - vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), + vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET): cv.time_period, @@ -82,22 +67,14 @@ def _convert_to_options_schema(hass, options): to_type = cv.multi_select(all_lights) elif value == cv.boolean: to_type = bool - elif ( - isinstance(value, vol.All) - and hasattr(value.validators, "type") - and value.validators[0].type == int - ) or value == VALID_TRANSITION: + elif (isinstance(value, vol.All) and hasattr(value.validators, "type") and value.validators[0].type == int) or value == VALID_TRANSITION: to_type = value elif value == cv.time_period: to_type = cv.time_period_dict else: to_type = str - default = ( - key.default() - if not isinstance(key.default, vol.Undefined) - else vol.UNDEFINED - ) + default = key.default() if not isinstance(key.default, vol.Undefined) else vol.UNDEFINED default = options.get(key.schema, default) schema[vol.Optional(key.schema, default=default)] = to_type return vol.Schema(schema) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d394199c..31b07e40 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -28,6 +28,7 @@ Technical notes: I had to make a lot of assumptions when writing this app """ import asyncio +import bisect import logging from datetime import timedelta @@ -35,6 +36,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util +from custom_components import adaptive_lighting from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, @@ -74,7 +76,6 @@ from homeassistant.util.color import ( ) from .const import ( - _COMMON_SCHEMA, CONF_DISABLE_BRIGHTNESS_ADJUST, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, @@ -95,6 +96,20 @@ from .const import ( 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, ICON, SUN_EVENT_MIDNIGHT, @@ -113,42 +128,15 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) -PLATFORM_SCHEMA = vol.Schema( - { - vol.Required(CONF_PLATFORM): DOMAIN, - vol.Optional(CONF_NAME, default="Adaptive Lighting"): cv.string, - **_COMMON_SCHEMA, - } -) - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Adaptive Lighting switches.""" - switch = AdaptiveSwitch( - hass, - name=config[CONF_NAME], - lights=config[CONF_LIGHTS], - disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST], - disable_entity=config.get(CONF_DISABLE_ENTITY), - disable_state=config.get(CONF_DISABLE_STATE), - initial_transition=config[CONF_INITIAL_TRANSITION], - interval=config[CONF_INTERVAL], - max_brightness=config[CONF_MAX_BRIGHTNESS], - max_color_temp=config[CONF_MAX_COLOR_TEMP], - min_brightness=config[CONF_MIN_BRIGHTNESS], - min_color_temp=config[CONF_MIN_COLOR_TEMP], - only_once=config[CONF_ONLY_ONCE], - sleep_brightness=config[CONF_SLEEP_BRIGHTNESS], - sleep_color_temp=config[CONF_SLEEP_COLOR_TEMP], - sleep_entity=config.get(CONF_SLEEP_ENTITY), - sleep_state=config.get(CONF_SLEEP_STATE), - sunrise_offset=config[CONF_SUNRISE_OFFSET], - sunrise_time=config.get(CONF_SUNRISE_TIME), - sunset_offset=config[CONF_SUNSET_OFFSET], - sunset_time=config.get(CONF_SUNSET_TIME), - transition=config[CONF_TRANSITION], - ) - add_devices([switch]) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the AdaptiveLighting switch.""" + name = config_entry.data[CONF_NAME] + switch = AdaptiveSwitch(hass, name, config_entry) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + hass.data[DOMAIN][name] = switch + async_add_entities([switch]) def _difference_between_states(from_state, to_state): @@ -191,54 +179,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self, hass, name, - lights, - disable_brightness_adjust, - disable_entity, - disable_state, - initial_transition, - interval, - max_brightness, - max_color_temp, - min_brightness, - min_color_temp, - only_once, - sleep_brightness, - sleep_color_temp, - sleep_entity, - sleep_state, - sunrise_offset, - sunrise_time, - sunset_offset, - sunset_time, - transition, + config_entry, ): """Initialize the Adaptive Lighting switch.""" self.hass = hass self._name = name - self._entity_id = f"switch.adaptive_lighting_{slugify(name)}" + self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" self._icon = ICON - - # Set attributes from arguments - self._lights = lights - self._disable_brightness_adjust = disable_brightness_adjust - self._disable_entity = disable_entity - self._disable_state = disable_state - self._initial_transition = initial_transition - self._interval = interval - self._max_brightness = max_brightness - self._max_color_temp = max_color_temp - self._min_brightness = min_brightness - self._min_color_temp = min_color_temp - self._only_once = only_once - self._sleep_brightness = sleep_brightness - self._sleep_color_temp = sleep_color_temp - self._sleep_entity = sleep_entity - self._sleep_state = sleep_state - self._sunrise_offset = sunrise_offset - self._sunrise_time = sunrise_time - self._sunset_offset = sunset_offset - self._sunset_time = sunset_time - self._transition = transition + self.config_entry = config_entry # Initialize attributes that will be set in self._update_attrs self._percent = None @@ -252,6 +200,104 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.unsub_tracker = None + @property + def _lights(self): + return self.config_entry.options.get(CONF_LIGHTS, DEFAULT_LIGHTS) + + @property + def _disable_brightness_adjust(self): + return self.config_entry.options.get( + CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST + ) + + @property + def _disable_entity(self): + return self.config_entry.options.get(CONF_DISABLE_ENTITY) + + @property + def _disable_state(self): + return self.config_entry.options.get(CONF_DISABLE_STATE) + + @property + def _initial_transition(self): + return self.config_entry.options.get( + CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION + ) + + @property + def _interval(self): + return self.config_entry.options.get(CONF_INTERVAL, DEFAULT_INTERVAL) + + @property + def _max_brightness(self): + return self.config_entry.options.get( + CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS + ) + + @property + def _max_color_temp(self): + return self.config_entry.options.get( + CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP + ) + + @property + def _min_brightness(self): + return self.config_entry.options.get( + CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS + ) + + @property + def _min_color_temp(self): + return self.config_entry.options.get( + CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP + ) + + @property + def _only_once(self): + return self.config_entry.options.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) + + @property + def _sleep_brightness(self): + return self.config_entry.options.get( + CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS + ) + + @property + def _sleep_color_temp(self): + return self.config_entry.options.get( + CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP + ) + + @property + def _sleep_entity(self): + return self.config_entry.options.get(CONF_SLEEP_ENTITY) + + @property + def _sleep_state(self): + return self.config_entry.options.get(CONF_SLEEP_STATE) + + @property + def _sunrise_offset(self): + return self.config_entry.options.get( + CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET + ) + + @property + def _sunrise_time(self): + return self.config_entry.options.get(CONF_SUNRISE_TIME) + + @property + def _sunset_offset(self): + return self.config_entry.options.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) + + @property + def _sunset_time(self): + return self.config_entry.options.get(CONF_SUNSET_TIME) + + @property + def _transition(self): + return self.config_entry.options.get(CONF_TRANSITION, DEFAULT_TRANSITION) + @property def entity_id(self): """Return the entity ID of the switch.""" @@ -411,34 +457,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): SUN_EVENT_MIDNIGHT: solar_midnight.timestamp(), } + def _relevant_events(self, now): + events = [] + for days in [-1, 0, 1]: + sun_events = self._get_sun_events(now + timedelta(days=days)) + events.extend(list(sun_events.items())) + events = sorted(events, key=lambda x: x[1]) + i_now = bisect.bisect([ts for _, ts in events], now.timestamp()) + return dict(events[i_now - 2: i_now + 2]) + def _calc_percent(self): now = dt_util.utcnow() now_ts = now.timestamp() - - today = self._get_sun_events(now) - if now_ts < today[SUN_EVENT_SUNRISE]: - # It's before sunrise (after midnight), because it's before - # sunrise (and after midnight) sunset must have happend yesterday. - yesterday = self._get_sun_events(now - timedelta(days=1)) - if ( - today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] - and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET] - ): - # Solar midnight is after sunset so use yesterdays's time - today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT] - today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] - elif now_ts > today[SUN_EVENT_SUNSET]: - # It's after sunset (before midnight), because it's after sunset - # (and before midnight) sunrise should happen tomorrow. - tomorrow = self._get_sun_events(now + timedelta(days=1)) - if ( - today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] - and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE] - ): - # Solar midnight is before sunrise so use tomorrow's time - today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT] - today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] - + today = self._relevant_events(now) # Figure out where we are in time so we know which half of the # parabola to calculate. We're generating a different # sunset-sunrise parabola for before and after solar midnight.