diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py new file mode 100644 index 00000000..c31e138b --- /dev/null +++ b/custom_components/circadian_lighting/__init__.py @@ -0,0 +1,301 @@ +""" +Circadian 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 circadian 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 logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.light import ( + VALID_TRANSITION, ATTR_TRANSITION) +from homeassistant.const import ( + CONF_LATITUDE, CONF_LONGITUDE, + SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) +from homeassistant.util import Throttle +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_sunrise, track_sunset, track_time_change +from homeassistant.util.color import ( + color_temperature_to_rgb, color_RGB_to_xy, + color_xy_to_hs) +from homeassistant.util.dt import utcnow as dt_utcnow, as_local + +import astral +from datetime import datetime, timedelta + +from .const import VERSION + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'circadian_lighting' +CIRCADIAN_LIGHTING_PLATFORMS = ['sensor', 'switch'] +CIRCADIAN_LIGHTING_UPDATE_TOPIC = '{0}_update'.format(DOMAIN) +DATA_CIRCADIAN_LIGHTING = 'data_cl' + +CONF_MIN_CT = 'min_colortemp' +DEFAULT_MIN_CT = 2500 +CONF_MAX_CT = 'max_colortemp' +DEFAULT_MAX_CT = 5500 +CONF_SUNRISE_OFFSET = 'sunrise_offset' +CONF_SUNSET_OFFSET = 'sunset_offset' +CONF_SUNRISE_TIME = 'sunrise_time' +CONF_SUNSET_TIME = 'sunset_time' +CONF_INTERVAL = 'interval' +DEFAULT_INTERVAL = 900 + +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_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int, + vol.Optional(ATTR_TRANSITION, default=DEFAULT_INTERVAL): VALID_TRANSITION + }), +}, extra=vol.ALLOW_EXTRA) + +def setup(hass, config): + """Set up the Circadian Lighting component.""" + conf = config[DOMAIN] + 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) + + if conf.get(CONF_LATITUDE) is None: + latitude = hass.config.latitude + else: + latitude = conf.get(CONF_LATITUDE) + + if conf.get(CONF_LONGITUDE) is None: + longitude = hass.config.longitude + else: + longitude = conf.get(CONF_LONGITUDE) + + load_platform(hass, 'sensor', DOMAIN, {}, config) + + interval = conf.get(CONF_INTERVAL) + transition = conf.get(ATTR_TRANSITION) + + cl = CircadianLighting(hass, min_colortemp, max_colortemp, + sunrise_offset, sunset_offset, sunrise_time, sunset_time, + latitude, longitude, + interval, transition) + + hass.data[DATA_CIRCADIAN_LIGHTING] = cl + + return True + +class CircadianLighting(object): + """Calculate universal Circadian values.""" + + def __init__(self, hass, min_colortemp, max_colortemp, + sunrise_offset, sunset_offset, sunrise_time, sunset_time, + latitude, longitude, + interval, transition): + self.hass = hass + self.data = {} + self.data['min_colortemp'] = min_colortemp + self.data['max_colortemp'] = max_colortemp + self.data['sunrise_offset'] = sunrise_offset + self.data['sunset_offset'] = sunset_offset + self.data['sunrise_time'] = sunrise_time + self.data['sunset_time'] = sunset_time + self.data['latitude'] = latitude + self.data['longitude'] = longitude + self.data['interval'] = interval + self.data['transition'] = transition + self.data['percent'] = self.calc_percent() + self.data['colortemp'] = self.calc_colortemp() + self.data['rgb_color'] = self.calc_rgb() + self.data['xy_color'] = self.calc_xy() + self.data['hs_color'] = self.calc_hs() + + self.update = Throttle(timedelta(seconds=interval))(self._update) + + if self.data['sunrise_time'] is not None: + track_time_change(self.hass, self._update, hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S"))) + else: + track_sunrise(self.hass, self._update, self.data['sunrise_offset']) + if self.data['sunset_time'] is not None: + track_time_change(self.hass, self._update, hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S"))) + else: + track_sunset(self.hass, self._update, self.data['sunset_offset']) + + def get_sunrise_sunset(self, date = None): + if self.data['sunrise_time'] is not None and self.data['sunset_time'] is not None: + if date is None: + utcdate = dt_utcnow() + date = as_local(utcdate) + sunrise = date.replace(hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S")), microsecond=int(self.data['sunrise_time'].strftime("%f"))) + sunset = date.replace(hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S")), microsecond=int(self.data['sunset_time'].strftime("%f"))) + solar_noon = sunrise + (sunset - sunrise)/2 + solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset)/2 + else: + location = astral.Location() + location.latitude = self.data['latitude'] + location.longitude = self.data['longitude'] + if self.data['sunrise_time'] is not None: + if date is None: + utcdate = dt_utcnow() + date = as_local(utcdate) + sunrise = date.replace(hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S")), microsecond=int(self.data['sunrise_time'].strftime("%f"))) + else: + sunrise = location.sunrise(date) + if self.data['sunset_time'] is not None: + if date is None: + utcdate = dt_utcnow() + date = as_local(utcdate) + sunset = date.replace(hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S")), microsecond=int(self.data['sunset_time'].strftime("%f"))) + else: + sunset = location.sunset(date) + solar_noon = location.solar_noon(date) + solar_midnight = location.solar_midnight(date) + if self.data['sunrise_offset'] is not None: + sunrise = sunrise + self.data['sunrise_offset'] + if self.data['sunset_offset'] is not None: + sunset = sunset + self.data['sunset_offset'] + return { + SUN_EVENT_SUNRISE: sunrise, + SUN_EVENT_SUNSET: sunset, + 'solar_noon': solar_noon, + 'solar_midnight': solar_midnight + } + + def calc_percent(self): + utcnow = dt_utcnow() + now = as_local(utcnow) + today_sun_times = self.get_sunrise_sunset() + + now_seconds = now.timestamp() + today_sunrise_seconds = today_sun_times[SUN_EVENT_SUNRISE].timestamp() + today_sunset_seconds = today_sun_times[SUN_EVENT_SUNSET].timestamp() + today_solar_noon_seconds = today_sun_times['solar_noon'].timestamp() + today_solar_midnight_seconds = today_sun_times['solar_midnight'].timestamp() + + if now < today_sun_times[SUN_EVENT_SUNRISE]: + yesterday_sun_times = self.get_sunrise_sunset(now - timedelta(days=1)) + yesterday_sunrise_seconds = yesterday_sun_times[SUN_EVENT_SUNRISE].timestamp() + yesterday_sunset_seconds = yesterday_sun_times[SUN_EVENT_SUNSET].timestamp() + yesterday_solar_midnight_seconds = yesterday_sun_times['solar_midnight'].timestamp() + + x1 = yesterday_sunset_seconds + y1 = 0 + + if today_sun_times['solar_midnight'] > yesterday_sun_times[SUN_EVENT_SUNSET] and today_sun_times['solar_midnight'] < today_sun_times[SUN_EVENT_SUNRISE]: + x2 = today_solar_midnight_seconds + else: + x2 = yesterday_solar_midnight_seconds + y2 = -100 + + x3 = today_sunrise_seconds + y3 = 0 + elif now > today_sun_times[SUN_EVENT_SUNSET]: + tomorrow_sun_times = self.get_sunrise_sunset(now + timedelta(days=1)) + tomorrow_sunrise_seconds = tomorrow_sun_times[SUN_EVENT_SUNRISE].timestamp() + tomorrow_sunset_seconds = tomorrow_sun_times[SUN_EVENT_SUNSET].timestamp() + tomorrow_solar_midnight_seconds = tomorrow_sun_times['solar_midnight'].timestamp() + + x1 = today_sunset_seconds + y1 = 0 + + if today_sun_times['solar_midnight'] > today_sun_times[SUN_EVENT_SUNSET] and today_sun_times['solar_midnight'] < tomorrow_sun_times[SUN_EVENT_SUNRISE]: + x2 = today_solar_midnight_seconds + else: + x2 = tomorrow_solar_midnight_seconds + y2 = -100 + + x3 = tomorrow_sunrise_seconds + y3 = 0 + else: + x1 = today_sunrise_seconds + y1 = 0 + x2 = today_solar_noon_seconds + y2 = 100 + x3 = today_sunset_seconds + y3 = 0 + + # Generate color temperature parabola from points + a1 = -x1**2+x2**2 + b1 = -x1+x2 + d1 = -y1+y2 + a2 = -x2**2+x3**2 + b2 = -x2+x3 + d2 = -y2+y3 + bm = -(b2/b1) + a3 = bm*a1+a2 + d3 = bm*d1+d2 + a = d3/a3 + b = (d1-a1*a)/b1 + c = y1-a*x1**2-b*x1 + percentage = a*now_seconds**2+b*now_seconds+c + + return percentage + + def calc_colortemp(self): + if self.data['percent'] > 0: + return ((self.data['max_colortemp'] - self.data['min_colortemp']) * (self.data['percent'] / 100)) + self.data['min_colortemp'] + else: + return self.data['min_colortemp'] + + def calc_rgb(self): + return color_temperature_to_rgb(self.data['colortemp']) + + def calc_xy(self): + rgb = self.calc_rgb() + iR = rgb[0] + iG = rgb[1] + iB = rgb[2] + + return color_RGB_to_xy(iR, iG, iB) + + def calc_hs(self): + xy = self.calc_xy() + vX = xy[0] + vY = xy[1] + + return color_xy_to_hs(vX, vY) + + def _update(self, *args, **kwargs): + """Update Circadian Values.""" + self.data['percent'] = self.calc_percent() + self.data['colortemp'] = self.calc_colortemp() + self.data['rgb_color'] = self.calc_rgb() + self.data['xy_color'] = self.calc_xy() + self.data['hs_color'] = self.calc_hs() + dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC) + _LOGGER.debug("Circadian Lighting Component Updated") diff --git a/custom_components/circadian_lighting/const.py b/custom_components/circadian_lighting/const.py new file mode 100644 index 00000000..88138c0f --- /dev/null +++ b/custom_components/circadian_lighting/const.py @@ -0,0 +1,3 @@ +# coding: utf-8 +"""Constants used by Circadian Lighting components.""" +VERSION = '1.0.0' \ No newline at end of file diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py new file mode 100644 index 00000000..a3d35045 --- /dev/null +++ b/custom_components/circadian_lighting/sensor.py @@ -0,0 +1,101 @@ +""" +Circadian Lighting Sensor for Home-Assistant. +""" + +DEPENDENCIES = ['circadian_lighting'] + +import logging + +from custom_components.circadian_lighting import DOMAIN, CIRCADIAN_LIGHTING_UPDATE_TOPIC, DATA_CIRCADIAN_LIGHTING + +from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.entity import Entity + +import datetime + +from .const import VERSION + +_LOGGER = logging.getLogger(__name__) + +ICON = 'mdi:theme-light-dark' + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Circadian Lighting sensor.""" + cl = hass.data.get(DATA_CIRCADIAN_LIGHTING) + if cl: + cs = CircadianSensor(hass, cl) + add_devices([cs]) + + def update(call=None): + """Update component.""" + cl._update() + service_name = "values_update" + hass.services.register(DOMAIN, service_name, update) + return True + else: + return False + +class CircadianSensor(Entity): + """Representation of a Circadian Lighting sensor.""" + + def __init__(self, hass, cl): + """Initialize the Circadian Lighting sensor.""" + self._cl = cl + self._name = 'Circadian Values' + self._entity_id = 'sensor.circadian_values' + self._state = self._cl.data['percent'] + self._unit_of_measurement = '%' + self._icon = ICON + self._hs_color = self._cl.data['hs_color'] + self._attributes = self._cl.data + + """Register callbacks.""" + dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_sensor) + + @property + def entity_id(self): + """Return the entity ID of the sensor.""" + return self._entity_id + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def hs_color(self): + return self._hs_color + + @property + def device_state_attributes(self): + """Return the attributes of the sensor.""" + return dict((k,str(v) if isinstance(v, datetime.time) or isinstance(v, datetime.timedelta) else v) for k,v in self._attributes.items()) + + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + self._cl.update() + + def update_sensor(self): + if self._cl.data is not None: + self._state = self._cl.data['percent'] + self._hs_color = self._cl.data['hs_color'] + self._attributes = self._cl.data + _LOGGER.debug("Circadian Lighting Sensor Updated") \ No newline at end of file diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py new file mode 100644 index 00000000..4420eba8 --- /dev/null +++ b/custom_components/circadian_lighting/switch.py @@ -0,0 +1,338 @@ +""" +Circadian Lighting Switch for Home-Assistant. +""" + +DEPENDENCIES = ['circadian_lighting', 'light'] + +import logging + +from custom_components.circadian_lighting import DOMAIN, CIRCADIAN_LIGHTING_UPDATE_TOPIC, DATA_CIRCADIAN_LIGHTING + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.components.light import ( + is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, STATE_ON, + SERVICE_TURN_ON) +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 VERSION + +_LOGGER = logging.getLogger(__name__) + +ICON = 'mdi:theme-light-dark' + +CONF_LIGHTS_CT = 'lights_ct' +CONF_LIGHTS_RGB = 'lights_rgb' +CONF_LIGHTS_XY = 'lights_xy' +CONF_LIGHTS_BRIGHT = 'lights_brightness' +CONF_DISABLE_BRIGHTNESS_ADJUST = 'disable_brightness_adjust' +CONF_MIN_BRIGHT = 'min_brightness' +DEFAULT_MIN_BRIGHT = 1 +CONF_MAX_BRIGHT = 'max_brightness' +DEFAULT_MAX_BRIGHT = 100 +CONF_SLEEP_ENTITY = 'sleep_entity' +CONF_SLEEP_STATE = 'sleep_state' +CONF_SLEEP_CT = 'sleep_colortemp' +CONF_SLEEP_BRIGHT = 'sleep_brightness' +CONF_DISABLE_ENTITY = 'disable_entity' +CONF_DISABLE_STATE = 'disable_state' + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'circadian_lighting', + vol.Optional(CONF_NAME, default="Circadian Lighting"): cv.string, + vol.Optional(CONF_LIGHTS_CT): cv.entity_ids, + vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, + vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, + vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, + vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): + vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): + vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, + vol.Optional(CONF_SLEEP_STATE): cv.string, + vol.Optional(CONF_SLEEP_CT): + vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_SLEEP_BRIGHT): + vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, + vol.Optional(CONF_DISABLE_STATE): cv.string +}) + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Circadian Lighting switches.""" + cl = hass.data.get(DATA_CIRCADIAN_LIGHTING) + if cl: + 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) + name = config.get(CONF_NAME) + 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) + cs = CircadianSwitch(hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, + disable_brightness_adjust, min_brightness, max_brightness, + sleep_entity, sleep_state, sleep_colortemp, sleep_brightness, + disable_entity, disable_state) + add_devices([cs]) + + def update(call=None): + """Update lights.""" + cs.update_switch() + return True + else: + return False + + +class CircadianSwitch(SwitchDevice, RestoreEntity): + """Representation of a Circadian Lighting switch.""" + + def __init__(self, hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, + disable_brightness_adjust, min_brightness, max_brightness, + sleep_entity, sleep_state, sleep_colortemp, sleep_brightness, + disable_entity, disable_state): + """Initialize the Circadian Lighting switch.""" + self.hass = hass + self._cl = cl + self._name = name + self._entity_id = "switch." + slugify("{} {}".format('circadian_lighting', name)) + self._state = None + self._icon = ICON + self._hs_color = None + self._attributes = {} + self._attributes['lights_ct'] = lights_ct + self._attributes['lights_rgb'] = lights_rgb + self._attributes['lights_xy'] = lights_xy + self._attributes['lights_brightness'] = lights_brightness + self._attributes['disable_brightness_adjust'] = disable_brightness_adjust + self._attributes['min_brightness'] = min_brightness + self._attributes['max_brightness'] = max_brightness + self._attributes['sleep_entity'] = sleep_entity + self._attributes['sleep_state'] = sleep_state + self._attributes['sleep_colortemp'] = sleep_colortemp + self._attributes['sleep_brightness'] = sleep_brightness + self._attributes['disable_entity'] = disable_entity + self._attributes['disable_state'] = disable_state + self._attributes['hs_color'] = self._hs_color + self._attributes['brightness'] = None + + self._lights = [] + if lights_ct != None: + self._lights += lights_ct + if lights_rgb != None: + self._lights += lights_rgb + if lights_xy != None: + self._lights += lights_xy + if lights_brightness != None: + self._lights += lights_brightness + + """Register callbacks.""" + dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_switch) + track_state_change(hass, self._lights, self.light_state_changed) + if self._attributes['sleep_entity'] is not None: + track_state_change(hass, self._attributes['sleep_entity'], self.sleep_state_changed) + + @property + def entity_id(self): + """Return the entity ID of the switch.""" + return self._entity_id + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if circadian lighting is on.""" + return self._state + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + # If not None, we got an initial value. + await super().async_added_to_hass() + if self._state is not None: + return + + state = await self.async_get_last_state() + self._state = state and state.state == STATE_ON + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def hs_color(self): + return self._hs_color + + @property + def device_state_attributes(self): + """Return the attributes of the switch.""" + return self._attributes + + def turn_on(self, **kwargs): + """Turn on circadian lighting.""" + self._state = True + + # Make initial update + self.update_switch() + + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn off circadian lighting.""" + self._state = False + self.schedule_update_ha_state() + self._hs_color = None + self._attributes['hs_color'] = self._hs_color + self._attributes['brightness'] = None + + def is_sleep(self): + return self._attributes['sleep_entity'] is not None and self.hass.states.get(self._attributes['sleep_entity']).state == self._attributes['sleep_state'] + + def calc_ct(self): + if self.is_sleep(): + _LOGGER.debug(self._name + " in Sleep mode") + return color_temperature_kelvin_to_mired(self._attributes['sleep_colortemp']) + else: + return color_temperature_kelvin_to_mired(self._cl.data['colortemp']) + + def calc_rgb(self): + if self.is_sleep(): + _LOGGER.debug(self._name + " in Sleep mode") + return color_temperature_to_rgb(self._attributes['sleep_colortemp']) + else: + return color_temperature_to_rgb(self._cl.data['colortemp']) + + def calc_xy(self): + return color_RGB_to_xy(*self.calc_rgb()) + + def calc_hs(self): + return color_xy_to_hs(*self.calc_xy()) + + def calc_brightness(self): + if self._attributes['disable_brightness_adjust'] is True: + return None + else: + if self.is_sleep(): + _LOGGER.debug(self._name + " in Sleep mode") + return self._attributes['sleep_brightness'] + else: + if self._cl.data['percent'] > 0: + return self._attributes['max_brightness'] + else: + return ((self._attributes['max_brightness'] - self._attributes['min_brightness']) * ((100+self._cl.data['percent']) / 100)) + self._attributes['min_brightness'] + + def update_switch(self, transition=None): + if self._cl.data is not None: + self._hs_color = self.calc_hs() + self._attributes['hs_color'] = self._hs_color + self._attributes['brightness'] = self.calc_brightness() + _LOGGER.debug(self._name + " Switch Updated") + + self.adjust_lights(self._lights, transition) + + def should_adjust(self): + if self._state is not True: + _LOGGER.debug(self._name + " off - not adjusting") + return False + elif self._cl.data is None: + _LOGGER.debug(self._name + " could not retrieve Circadian Lighting data") + return False + elif self._attributes['disable_entity'] is not None and self.hass.states.get(self._attributes['disable_entity']).state == self._attributes['disable_state']: + _LOGGER.debug(self._name + " disabled by " + str(self._attributes['disable_entity'])) + return False + else: + return True + + def adjust_lights(self, lights, transition=None): + if self.should_adjust(): + if transition == None: + transition = self._cl.data['transition'] + + brightness = (self._attributes['brightness'] / 100) * 255 if self._attributes['brightness'] is not None else None + + for light in lights: + """Set color of array of ct light.""" + if self._attributes['lights_ct'] is not None and light in self._attributes['lights_ct']: + mired = int(self.calc_ct()) + if is_on(self.hass, light): + service_data = {ATTR_ENTITY_ID: light} + if mired is not None: + service_data[ATTR_COLOR_TEMP] = int(mired) + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) + + """Set color of array of rgb light.""" + if self._attributes['lights_rgb'] is not None and light in self._attributes['lights_rgb']: + rgb = self.calc_rgb() + if is_on(self.hass, light): + service_data = {ATTR_ENTITY_ID: light} + if rgb is not None: + service_data[ATTR_RGB_COLOR] = rgb + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) + + """Set color of array of xy light.""" + if self._attributes['lights_xy'] is not None and light in self._attributes['lights_xy']: + x_val, y_val = self.calc_xy() + if is_on(self.hass, light): + service_data = {ATTR_ENTITY_ID: light} + if x_val is not None and y_val is not None: + service_data[ATTR_XY_COLOR] = [x_val, y_val] + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + service_data[ATTR_WHITE_VALUE] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " XY Adjusted - xy_color: [" + str(x_val) + ", " + str(y_val) + "], brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) + + """Set color of array of brightness light.""" + if self._attributes['lights_brightness'] is not None and light in self._attributes['lights_brightness']: + if is_on(self.hass, light): + service_data = {ATTR_ENTITY_ID: light} + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) + + def light_state_changed(self, entity_id, from_state, to_state): + self.adjust_lights([entity_id], 1) + + def sleep_state_changed(self, entity_id, from_state, to_state): + if to_state.state == self._attributes['sleep_state'] or from_state.state == self._attributes['sleep_state']: + self.update_switch(1) \ No newline at end of file diff --git a/custom_updater.json b/custom_updater.json new file mode 100644 index 00000000..c105a459 --- /dev/null +++ b/custom_updater.json @@ -0,0 +1,14 @@ +{ + "circadian_lighting": { + "version": "1.0.0", + "local_location": "/custom_components/circadian_lighting/__init__.py", + "remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py", + "visit_repo": "https://github.com/claytonjn/hass-circadian_lighting", + "changelog": "https://github.com/claytonjn/hass-circadian_lighting/releases", + "resources": [ + "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/const.py", + "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/sensor.py", + "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/switch.py" + ] + } +} \ No newline at end of file