mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-11 22:34:04 +02:00
Initial push
with new folder structure and custom_updater support (hopefully)
This commit is contained in:
parent
7e29b7c640
commit
e80f45b90b
5 changed files with 757 additions and 0 deletions
301
custom_components/circadian_lighting/__init__.py
Normal file
301
custom_components/circadian_lighting/__init__.py
Normal file
|
|
@ -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")
|
||||
3
custom_components/circadian_lighting/const.py
Normal file
3
custom_components/circadian_lighting/const.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# coding: utf-8
|
||||
"""Constants used by Circadian Lighting components."""
|
||||
VERSION = '1.0.0'
|
||||
101
custom_components/circadian_lighting/sensor.py
Normal file
101
custom_components/circadian_lighting/sensor.py
Normal file
|
|
@ -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")
|
||||
338
custom_components/circadian_lighting/switch.py
Normal file
338
custom_components/circadian_lighting/switch.py
Normal file
|
|
@ -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)
|
||||
14
custom_updater.json
Normal file
14
custom_updater.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue