diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 24b4fcb1..a59ea651 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -27,6 +27,7 @@ Technical notes: I had to make a lot of assumptions when writing this app lights to 2700K (warm white) until your hub goes into Night mode """ +import bisect import logging from datetime import timedelta @@ -72,52 +73,66 @@ CONF_SUNSET_OFFSET = "sunset_offset" CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_TIME = "sunset_time" DEFAULT_TRANSITION = 60 +CONF_PROFILE, DEFAULT_PROFILE = "profile", "default" + +_DOMAIN_SCHEMA = vol.Schema( + { + vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNRISE_TIME): cv.time, + vol.Optional(CONF_SUNSET_TIME): cv.time, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_ELEVATION): float, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, + vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, + vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): cv.string, + } +) + + +def _all_unique_profiles(value): + """Validate that all enties have a unique profile name.""" + hosts = [device[CONF_PROFILE] for device in value] + schema = vol.Schema(vol.Unique()) + schema(hosts) + return value + CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_TIME): cv.time, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional( - ATTR_TRANSITION, default=DEFAULT_TRANSITION - ): VALID_TRANSITION, - } - ), - }, + {DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)}, extra=vol.ALLOW_EXTRA, ) def setup(hass, config): """Set up the Circadian Lighting platform.""" - conf = config[DOMAIN] - hass.data[DOMAIN] = CircadianLighting( - hass, - min_colortemp=conf.get(CONF_MIN_CT), - max_colortemp=conf.get(CONF_MAX_CT), - sunrise_offset=conf.get(CONF_SUNRISE_OFFSET), - sunset_offset=conf.get(CONF_SUNSET_OFFSET), - sunrise_time=conf.get(CONF_SUNRISE_TIME), - sunset_time=conf.get(CONF_SUNSET_TIME), - latitude=conf.get(CONF_LATITUDE, hass.config.latitude), - longitude=conf.get(CONF_LONGITUDE, hass.config.longitude), - elevation=conf.get(CONF_ELEVATION, hass.config.elevation), - interval=conf.get(CONF_INTERVAL), - transition=conf.get(ATTR_TRANSITION), - ) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + configs = config[DOMAIN] + for conf in configs: + profile = conf[CONF_PROFILE] + hass.data[DOMAIN][profile] = CircadianLighting( + hass, + min_colortemp=conf[CONF_MIN_CT], + max_colortemp=conf[CONF_MAX_CT], + sunrise_offset=conf.get(CONF_SUNRISE_OFFSET), + sunset_offset=conf.get(CONF_SUNSET_OFFSET), + sunrise_time=conf.get(CONF_SUNRISE_TIME), + sunset_time=conf.get(CONF_SUNSET_TIME), + latitude=conf.get(CONF_LATITUDE, hass.config.latitude), + longitude=conf.get(CONF_LONGITUDE, hass.config.longitude), + elevation=conf.get(CONF_ELEVATION, hass.config.elevation), + interval=conf[CONF_INTERVAL], + transition=conf[ATTR_TRANSITION], + profile=profile, + ) load_platform(hass, "sensor", DOMAIN, {}, config) return True @@ -140,6 +155,7 @@ class CircadianLighting: elevation, interval, transition, + profile, ): self.hass = hass self._min_colortemp = min_colortemp @@ -152,8 +168,10 @@ class CircadianLighting: self._longitude = longitude self._elevation = elevation self._transition = transition + self._profile = profile + _LOGGER.debug("profile: %s", self._profile) - self._percent = self.calc_percent() + self._percent = self._calc_percent() self._colortemp = self.calc_colortemp() self._rgb_color = self.calc_rgb() self._xy_color = self.calc_xy() @@ -192,7 +210,7 @@ class CircadianLighting: microsecond=other_date.microsecond, ) - def get_sunrise_sunset(self, date): + def _get_sun_events(self, date): if self._manual_sunrise is not None and self._manual_sunset is not None: sunrise = self._replace_time(date, "sunrise") sunset = self._replace_time(date, "sunset") @@ -235,34 +253,19 @@ class CircadianLighting: k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items() } - def calc_percent(self): + 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]) + index_now = bisect.bisect([ts for _, ts in events], now.timestamp()) + return dict(events[index_now - 2 : index_now + 2]) + + def _calc_percent(self): now = dt_util.utcnow() now_ts = now.timestamp() - - today = self.get_sunrise_sunset(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_sunrise_sunset(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_sunrise_sunset(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. @@ -315,7 +318,7 @@ class CircadianLighting: async def update(self, _=None): """Update Circadian Values.""" - self._percent = self.calc_percent() + self._percent = self._calc_percent() self._colortemp = self.calc_colortemp() self._rgb_color = self.calc_rgb() self._xy_color = self.calc_xy() diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 35947f92..4461fa6a 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -6,27 +6,19 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN +from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DEFAULT_PROFILE, DOMAIN ICON = "mdi:theme-light-dark" def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting sensor.""" - circadian_lighting = hass.data.get(DOMAIN) - if circadian_lighting is not None: - sensor = CircadianSensor(hass, circadian_lighting) - add_devices([sensor], True) - - def update(call=None): - """Update component.""" - circadian_lighting.update() - - service_name = "values_update" - hass.services.register(DOMAIN, service_name, update) - return True - else: - return False + sensors = [ + CircadianSensor(hass, circadian_lighting) + for circadian_lighting in hass.data[DOMAIN].values() + ] + add_devices(sensors, True) + return True class CircadianSensor(Entity): @@ -37,6 +29,10 @@ class CircadianSensor(Entity): self._circadian_lighting = circadian_lighting self._name = "Circadian Values" self._entity_id = "sensor.circadian_values" + profile = circadian_lighting._profile + if profile != DEFAULT_PROFILE: + self._name += f" {profile}" + self._entity_id += f"_{profile.lower()}" self._unit_of_measurement = "%" self._icon = ICON diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 7c5b6cc5..62366ad4 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -38,7 +38,7 @@ from homeassistant.util.color import ( color_xy_to_hs, ) -from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN +from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN, CONF_PROFILE, DEFAULT_PROFILE _LOGGER = logging.getLogger(__name__) @@ -89,39 +89,36 @@ PLATFORM_SCHEMA = vol.Schema( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean, + vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): cv.string, } ) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting switches.""" - circadian_lighting = hass.data.get(DOMAIN) - if circadian_lighting is not None: - switch = CircadianSwitch( - hass, - circadian_lighting, - name=config.get(CONF_NAME), - lights_ct=config.get(CONF_LIGHTS_CT, []), - lights_rgb=config.get(CONF_LIGHTS_RGB, []), - lights_xy=config.get(CONF_LIGHTS_XY, []), - lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), - disable_brightness_adjust=config.get(CONF_DISABLE_BRIGHTNESS_ADJUST), - min_brightness=config.get(CONF_MIN_BRIGHT), - max_brightness=config.get(CONF_MAX_BRIGHT), - sleep_entity=config.get(CONF_SLEEP_ENTITY), - sleep_state=config.get(CONF_SLEEP_STATE), - sleep_colortemp=config.get(CONF_SLEEP_CT), - sleep_brightness=config.get(CONF_SLEEP_BRIGHT), - disable_entity=config.get(CONF_DISABLE_ENTITY), - disable_state=config.get(CONF_DISABLE_STATE), - initial_transition=config.get(CONF_INITIAL_TRANSITION), - only_once=config.get(CONF_ONLY_ONCE), - ) - add_devices([switch]) - - return True - else: - return False + profile = config[CONF_PROFILE] + circadian_lighting = hass.data[DOMAIN][profile] + switch = CircadianSwitch( + hass, + circadian_lighting, + name=config[CONF_NAME], + lights_ct=config.get(CONF_LIGHTS_CT, []), + lights_rgb=config.get(CONF_LIGHTS_RGB, []), + lights_xy=config.get(CONF_LIGHTS_XY, []), + lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), + disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST], + min_brightness=config[CONF_MIN_BRIGHT], + max_brightness=config[CONF_MAX_BRIGHT], + sleep_entity=config.get(CONF_SLEEP_ENTITY), + sleep_state=config.get(CONF_SLEEP_STATE), + sleep_colortemp=config[CONF_SLEEP_CT], + sleep_brightness=config[CONF_SLEEP_BRIGHT], + disable_entity=config.get(CONF_DISABLE_ENTITY), + disable_state=config.get(CONF_DISABLE_STATE), + initial_transition=config[CONF_INITIAL_TRANSITION], + only_once=config[CONF_ONLY_ONCE], + ) + add_devices([switch]) def _difference_between_states(from_state, to_state):