mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-12 06:44:04 +02:00
move to single switch
This commit is contained in:
parent
b16d69b1c9
commit
4aff61d8cb
3 changed files with 335 additions and 160 deletions
|
|
@ -76,12 +76,8 @@ 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_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,
|
||||
|
|
@ -104,10 +100,7 @@ def _all_unique_profiles(value):
|
|||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)}, extra=vol.ALLOW_EXTRA,)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
@ -141,20 +134,7 @@ class CircadianLighting:
|
|||
"""Calculate universal Circadian values."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
min_colortemp,
|
||||
max_colortemp,
|
||||
sunrise_offset,
|
||||
sunset_offset,
|
||||
sunrise_time,
|
||||
sunset_time,
|
||||
latitude,
|
||||
longitude,
|
||||
elevation,
|
||||
interval,
|
||||
transition,
|
||||
profile,
|
||||
self, hass, min_colortemp, max_colortemp, sunrise_offset, sunset_offset, sunrise_time, sunset_time, latitude, longitude, elevation, interval, transition, profile,
|
||||
):
|
||||
self.hass = hass
|
||||
self._min_colortemp = min_colortemp
|
||||
|
|
@ -178,22 +158,14 @@ class CircadianLighting:
|
|||
|
||||
if self._manual_sunrise is not None:
|
||||
async_track_time_change(
|
||||
self.hass,
|
||||
self.update,
|
||||
hour=self._manual_sunrise.hour,
|
||||
minute=self._manual_sunrise.minute,
|
||||
second=self._manual_sunrise.second,
|
||||
self.hass, self.update, hour=self._manual_sunrise.hour, minute=self._manual_sunrise.minute, second=self._manual_sunrise.second,
|
||||
)
|
||||
else:
|
||||
async_track_sunrise(self.hass, self.update, self._sunrise_offset)
|
||||
|
||||
if self._manual_sunset is not None:
|
||||
async_track_time_change(
|
||||
self.hass,
|
||||
self.update,
|
||||
hour=self._manual_sunset.hour,
|
||||
minute=self._manual_sunset.minute,
|
||||
second=self._manual_sunset.second,
|
||||
self.hass, self.update, hour=self._manual_sunset.hour, minute=self._manual_sunset.minute, second=self._manual_sunset.second,
|
||||
)
|
||||
else:
|
||||
async_track_sunset(self.hass, self.update, self._sunset_offset)
|
||||
|
|
@ -202,12 +174,7 @@ class CircadianLighting:
|
|||
|
||||
def _replace_time(self, date, key):
|
||||
other_date = self._manual_sunrise if key == "sunrise" else self._manual_sunset
|
||||
return date.replace(
|
||||
hour=other_date.hour,
|
||||
minute=other_date.minute,
|
||||
second=other_date.second,
|
||||
microsecond=other_date.microsecond,
|
||||
)
|
||||
return date.replace(hour=other_date.hour, minute=other_date.minute, second=other_date.second, microsecond=other_date.microsecond,)
|
||||
|
||||
def get_sunrise_sunset(self, date):
|
||||
if self._manual_sunrise is not None and self._manual_sunset is not None:
|
||||
|
|
@ -248,9 +215,7 @@ class CircadianLighting:
|
|||
SUN_EVENT_MIDNIGHT: solar_midnight,
|
||||
}
|
||||
|
||||
return {
|
||||
k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items()
|
||||
}
|
||||
return {k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items()}
|
||||
|
||||
def calc_percent(self):
|
||||
now = dt_util.utcnow()
|
||||
|
|
@ -261,10 +226,7 @@ class CircadianLighting:
|
|||
# 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]
|
||||
):
|
||||
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]
|
||||
|
|
@ -272,10 +234,7 @@ class CircadianLighting:
|
|||
# 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]
|
||||
):
|
||||
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]
|
||||
|
|
@ -291,22 +250,14 @@ class CircadianLighting:
|
|||
h = today[SUN_EVENT_NOON]
|
||||
k = 100
|
||||
# parabola before solar_noon else after solar_noon
|
||||
x = (
|
||||
today[SUN_EVENT_SUNRISE]
|
||||
if now_ts < today[SUN_EVENT_NOON]
|
||||
else today[SUN_EVENT_SUNSET]
|
||||
)
|
||||
x = today[SUN_EVENT_SUNRISE] if now_ts < today[SUN_EVENT_NOON] else today[SUN_EVENT_SUNSET]
|
||||
|
||||
# sunset -> sunrise parabola
|
||||
elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]:
|
||||
h = today[SUN_EVENT_MIDNIGHT]
|
||||
k = -100
|
||||
# parabola before solar_midnight else after solar_midnight
|
||||
x = (
|
||||
today[SUN_EVENT_SUNSET]
|
||||
if now_ts < today[SUN_EVENT_MIDNIGHT]
|
||||
else today[SUN_EVENT_SUNRISE]
|
||||
)
|
||||
x = today[SUN_EVENT_SUNSET] if now_ts < today[SUN_EVENT_MIDNIGHT] else today[SUN_EVENT_SUNRISE]
|
||||
|
||||
y = 0
|
||||
a = (y - k) / (h - x) ** 2
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@ ICON = "mdi:theme-light-dark"
|
|||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Circadian Lighting sensor."""
|
||||
sensors = [
|
||||
CircadianSensor(hass, circadian_lighting)
|
||||
for circadian_lighting in hass.data[DOMAIN].values()
|
||||
]
|
||||
sensors = [CircadianSensor(hass, circadian_lighting) for circadian_lighting in hass.data[DOMAIN].values()]
|
||||
add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
|
@ -81,11 +78,7 @@ class CircadianSensor(Entity):
|
|||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Connect dispatcher to signal from CircadianLighting object."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_callback
|
||||
)
|
||||
)
|
||||
self.async_on_remove(async_dispatcher_connect(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_callback))
|
||||
|
||||
@callback
|
||||
def _update_callback(self) -> None:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,42 @@
|
|||
"""
|
||||
Circadian Lighting Switch for Home-Assistant.
|
||||
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 asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from itertools import repeat
|
||||
|
||||
import astral
|
||||
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,
|
||||
ATTR_COLOR_TEMP,
|
||||
|
|
@ -22,13 +50,23 @@ from homeassistant.components.light import VALID_TRANSITION, is_on
|
|||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_ELEVATION,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
SUN_EVENT_SUNRISE,
|
||||
SUN_EVENT_SUNSET,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change,
|
||||
async_track_sunrise,
|
||||
async_track_sunset,
|
||||
async_track_time_change,
|
||||
async_track_time_interval,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.color import (
|
||||
|
|
@ -38,85 +76,118 @@ from homeassistant.util.color import (
|
|||
color_xy_to_hs,
|
||||
)
|
||||
|
||||
from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN, CONF_PROFILE, DEFAULT_PROFILE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ICON = "mdi:theme-light-dark"
|
||||
|
||||
DOMAIN = "circadian_lighting"
|
||||
SUN_EVENT_NOON = "solar_noon"
|
||||
SUN_EVENT_MIDNIGHT = "solar_midnight"
|
||||
|
||||
CONF_LIGHTS_BRIGHT = "lights_brightness"
|
||||
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, DEFAULT_MIN_BRIGHT = "min_brightness", 1
|
||||
CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100
|
||||
CONF_SLEEP_ENTITY = "sleep_entity"
|
||||
CONF_SLEEP_STATE = "sleep_state"
|
||||
CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000
|
||||
CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1
|
||||
CONF_DISABLE_ENTITY = "disable_entity"
|
||||
CONF_DISABLE_STATE = "disable_state"
|
||||
CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1
|
||||
CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 300
|
||||
CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100
|
||||
CONF_MAX_CT, DEFAULT_MAX_CT = "max_colortemp", 5500
|
||||
CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1
|
||||
CONF_MIN_CT, DEFAULT_MIN_CT = "min_colortemp", 2500
|
||||
CONF_ONLY_ONCE = "only_once"
|
||||
CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1
|
||||
CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000
|
||||
CONF_SLEEP_ENTITY = "sleep_entity"
|
||||
CONF_SLEEP_STATE = "sleep_state"
|
||||
CONF_SUNRISE_OFFSET = "sunrise_offset"
|
||||
CONF_SUNRISE_TIME = "sunrise_time"
|
||||
CONF_SUNSET_OFFSET = "sunset_offset"
|
||||
CONF_SUNSET_TIME = "sunset_time"
|
||||
DEFAULT_TRANSITION = 60
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "circadian_lighting",
|
||||
vol.Optional(CONF_NAME, default="Circadian Lighting"): cv.string,
|
||||
vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids,
|
||||
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): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1000, max=10000)
|
||||
),
|
||||
vol.Optional(CONF_SLEEP_BRIGHT, default=DEFAULT_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): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_ELEVATION): float,
|
||||
vol.Optional(
|
||||
CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION
|
||||
): VALID_TRANSITION,
|
||||
vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period,
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=100)
|
||||
),
|
||||
vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1000, max=10000)
|
||||
),
|
||||
vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=100)
|
||||
),
|
||||
vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1000, max=10000)
|
||||
),
|
||||
vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): cv.string,
|
||||
vol.Optional(CONF_SLEEP_BRIGHT, default=DEFAULT_SLEEP_BRIGHT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=100)
|
||||
),
|
||||
vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): 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): cv.time_period_str,
|
||||
vol.Optional(CONF_SUNRISE_TIME): cv.time,
|
||||
vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str,
|
||||
vol.Optional(CONF_SUNSET_TIME): cv.time,
|
||||
vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Circadian Lighting switches."""
|
||||
profile = config[CONF_PROFILE]
|
||||
circadian_lighting = hass.data[DOMAIN][profile]
|
||||
switch = CircadianSwitch(
|
||||
hass,
|
||||
circadian_lighting,
|
||||
name=config[CONF_NAME],
|
||||
lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []),
|
||||
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),
|
||||
elevation=config.get(CONF_ELEVATION, hass.config.elevation),
|
||||
initial_transition=config[CONF_INITIAL_TRANSITION],
|
||||
interval=config[CONF_INTERVAL],
|
||||
latitude=config.get(CONF_LATITUDE, hass.config.latitude),
|
||||
longitude=config.get(CONF_LONGITUDE, hass.config.longitude),
|
||||
max_brightness=config[CONF_MAX_BRIGHT],
|
||||
max_colortemp=config[CONF_MAX_CT],
|
||||
min_brightness=config[CONF_MIN_BRIGHT],
|
||||
min_colortemp=config[CONF_MIN_CT],
|
||||
only_once=config[CONF_ONLY_ONCE],
|
||||
sleep_brightness=config[CONF_SLEEP_BRIGHT],
|
||||
sleep_colortemp=config[CONF_SLEEP_CT],
|
||||
sleep_entity=config.get(CONF_SLEEP_ENTITY),
|
||||
sleep_state=config.get(CONF_SLEEP_STATE),
|
||||
sunrise_offset=config.get(CONF_SUNRISE_OFFSET),
|
||||
sunrise_time=config.get(CONF_SUNRISE_TIME),
|
||||
sunset_offset=config.get(CONF_SUNSET_OFFSET),
|
||||
sunset_time=config.get(CONF_SUNSET_TIME),
|
||||
transition=config[ATTR_TRANSITION],
|
||||
)
|
||||
add_devices([switch])
|
||||
|
||||
|
|
@ -160,50 +231,77 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
|
|||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
circadian_lighting,
|
||||
name,
|
||||
lights_brightness,
|
||||
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,
|
||||
elevation,
|
||||
initial_transition,
|
||||
interval,
|
||||
latitude,
|
||||
longitude,
|
||||
max_brightness,
|
||||
max_colortemp,
|
||||
min_brightness,
|
||||
min_colortemp,
|
||||
only_once,
|
||||
sleep_brightness,
|
||||
sleep_colortemp,
|
||||
sleep_entity,
|
||||
sleep_state,
|
||||
sunrise_offset,
|
||||
sunrise_time,
|
||||
sunset_offset,
|
||||
sunset_time,
|
||||
transition,
|
||||
):
|
||||
"""Initialize the Circadian Lighting switch."""
|
||||
self.hass = hass
|
||||
self._circadian_lighting = circadian_lighting
|
||||
self._name = name
|
||||
self._entity_id = f"switch.circadian_lighting_{slugify(name)}"
|
||||
self._state = None
|
||||
self._icon = ICON
|
||||
self._hs_color = None
|
||||
self._brightness = None
|
||||
self._disable_brightness_adjust = disable_brightness_adjust
|
||||
self._min_brightness = min_brightness
|
||||
self._max_brightness = max_brightness
|
||||
self._sleep_entity = sleep_entity
|
||||
self._sleep_state = sleep_state
|
||||
self._sleep_colortemp = sleep_colortemp
|
||||
self._sleep_brightness = sleep_brightness
|
||||
self._disable_entity = disable_entity
|
||||
self._disable_state = disable_state
|
||||
self._initial_transition = initial_transition
|
||||
self._only_once = only_once
|
||||
self._lights_types = dict(zip(lights_ct, repeat("ct")))
|
||||
self._lights_types.update(zip(lights_brightness, repeat("brightness")))
|
||||
self._lights_types.update(zip(lights_rgb, repeat("rgb")))
|
||||
self._lights_types.update(zip(lights_xy, repeat("xy")))
|
||||
self._lights_types.update(zip(lights_brightness, repeat("brightness")))
|
||||
self._lights = list(self._lights_types.keys())
|
||||
|
||||
self._disable_brightness_adjust = disable_brightness_adjust
|
||||
self._disable_entity = disable_entity
|
||||
self._disable_state = disable_state
|
||||
self._elevation = elevation
|
||||
self._initial_transition = initial_transition
|
||||
self._interval = interval
|
||||
self._latitude = latitude
|
||||
self._longitude = longitude
|
||||
self._max_brightness = max_brightness
|
||||
self._max_colortemp = max_colortemp
|
||||
self._min_brightness = min_brightness
|
||||
self._min_colortemp = min_colortemp
|
||||
self._only_once = only_once
|
||||
self._sleep_brightness = sleep_brightness
|
||||
self._sleep_colortemp = sleep_colortemp
|
||||
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._percent = None
|
||||
self._brightness = None
|
||||
self._colortemp_kelvin = None
|
||||
self._colortemp_mired = None
|
||||
self._rgb_color = None
|
||||
self._xy_color = None
|
||||
self._hs_color = None
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""Return the entity ID of the switch."""
|
||||
|
|
@ -221,13 +319,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
|
|||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to hass."""
|
||||
# Add callback
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_switch
|
||||
)
|
||||
)
|
||||
|
||||
# Add listeners
|
||||
async_track_state_change(
|
||||
self.hass, self._lights, self._light_state_changed, to_state="on"
|
||||
|
|
@ -245,6 +336,32 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
|
|||
from_state=self._disable_state,
|
||||
)
|
||||
|
||||
# XXX FROM __INIT__ \/
|
||||
if self._manual_sunrise is not None:
|
||||
async_track_time_change(
|
||||
self.hass,
|
||||
self.update,
|
||||
hour=self._manual_sunrise.hour,
|
||||
minute=self._manual_sunrise.minute,
|
||||
second=self._manual_sunrise.second,
|
||||
)
|
||||
else:
|
||||
async_track_sunrise(self.hass, self.update, self._sunrise_offset)
|
||||
|
||||
if self._manual_sunset is not None:
|
||||
async_track_time_change(
|
||||
self.hass,
|
||||
self.update,
|
||||
hour=self._manual_sunset.hour,
|
||||
minute=self._manual_sunset.minute,
|
||||
second=self._manual_sunset.second,
|
||||
)
|
||||
else:
|
||||
async_track_sunset(self.hass, self.update, self._sunset_offset)
|
||||
|
||||
async_track_time_interval(self.hass, self.update, self._interval)
|
||||
# XXX FROM __INIT__ ^
|
||||
|
||||
if self._state is not None: # If not None, we got an initial value
|
||||
return
|
||||
|
||||
|
|
@ -276,47 +393,161 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
|
|||
self._hs_color = None
|
||||
self._brightness = None
|
||||
|
||||
def _replace_time(self, date, key):
|
||||
other_date = self._manual_sunrise if key == "sunrise" else self._manual_sunset
|
||||
return date.replace(
|
||||
hour=other_date.hour,
|
||||
minute=other_date.minute,
|
||||
second=other_date.second,
|
||||
microsecond=other_date.microsecond,
|
||||
)
|
||||
|
||||
def get_sunrise_sunset(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")
|
||||
solar_noon = sunrise + (sunset - sunrise) / 2
|
||||
solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2
|
||||
else:
|
||||
location = astral.Location()
|
||||
location.name = "name"
|
||||
location.region = "region"
|
||||
location.latitude = self._latitude
|
||||
location.longitude = self._longitude
|
||||
location.elevation = self._elevation
|
||||
|
||||
if self._manual_sunrise is not None:
|
||||
sunrise = self._replace_time(date, "sunrise")
|
||||
else:
|
||||
sunrise = location.sunrise(date)
|
||||
|
||||
if self._manual_sunset is not None:
|
||||
sunset = self._replace_time(date, "sunset")
|
||||
else:
|
||||
sunset = location.sunset(date)
|
||||
|
||||
solar_noon = location.solar_noon(date)
|
||||
solar_midnight = location.solar_midnight(date)
|
||||
|
||||
if self._sunrise_offset is not None:
|
||||
sunrise = sunrise + self._sunrise_offset
|
||||
if self._sunset_offset is not None:
|
||||
sunset = sunset + self._sunset_offset
|
||||
|
||||
datetimes = {
|
||||
SUN_EVENT_SUNRISE: sunrise,
|
||||
SUN_EVENT_SUNSET: sunset,
|
||||
SUN_EVENT_NOON: solar_noon,
|
||||
SUN_EVENT_MIDNIGHT: solar_midnight,
|
||||
}
|
||||
|
||||
return {
|
||||
k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items()
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
# 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.
|
||||
# because it might not be half way between sunrise and sunset.
|
||||
# We're also generating a different parabola for sunrise-sunset.
|
||||
|
||||
# sunrise -> sunset parabola
|
||||
if today[SUN_EVENT_SUNRISE] < now_ts < today[SUN_EVENT_SUNSET]:
|
||||
h = today[SUN_EVENT_NOON]
|
||||
k = 100
|
||||
# parabola before solar_noon else after solar_noon
|
||||
x = (
|
||||
today[SUN_EVENT_SUNRISE]
|
||||
if now_ts < today[SUN_EVENT_NOON]
|
||||
else today[SUN_EVENT_SUNSET]
|
||||
)
|
||||
|
||||
# sunset -> sunrise parabola
|
||||
elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]:
|
||||
h = today[SUN_EVENT_MIDNIGHT]
|
||||
k = -100
|
||||
# parabola before solar_midnight else after solar_midnight
|
||||
x = (
|
||||
today[SUN_EVENT_SUNSET]
|
||||
if now_ts < today[SUN_EVENT_MIDNIGHT]
|
||||
else today[SUN_EVENT_SUNRISE]
|
||||
)
|
||||
|
||||
y = 0
|
||||
a = (y - k) / (h - x) ** 2
|
||||
percentage = a * (now_ts - h) ** 2 + k
|
||||
return percentage
|
||||
|
||||
async def update(self, _=None): # from __init__
|
||||
"""Update Circadian Values."""
|
||||
self._percent = self._calc_percent()
|
||||
self._brightness = self._calc_brightness()
|
||||
self._colortemp_kelvin = self._calc_colortemp_kelvin()
|
||||
self._colortemp_mired = color_temperature_kelvin_to_mired(
|
||||
self._colortemp_kelvin
|
||||
)
|
||||
self._rgb_color = color_temperature_to_rgb(self._colortemp_kelvin)
|
||||
self._xy_color = color_RGB_to_xy(*self._rgb_color)
|
||||
self._hs_color = color_xy_to_hs(*self._xy_color)
|
||||
|
||||
def _is_sleep(self):
|
||||
return (
|
||||
self._sleep_entity is not None
|
||||
and self.hass.states.get(self._sleep_entity).state in self._sleep_state
|
||||
)
|
||||
|
||||
def _color_temperature(self):
|
||||
return (
|
||||
self._circadian_lighting._colortemp
|
||||
if not self._is_sleep()
|
||||
else self._sleep_colortemp
|
||||
)
|
||||
|
||||
def _calc_ct(self):
|
||||
return color_temperature_kelvin_to_mired(self._color_temperature())
|
||||
|
||||
def _calc_rgb(self):
|
||||
return color_temperature_to_rgb(self._color_temperature())
|
||||
|
||||
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_colortemp_kelvin(self):
|
||||
if self._is_sleep():
|
||||
return self._sleep_colortemp
|
||||
if self._percent > 0:
|
||||
delta = self._max_colortemp - self._min_colortemp
|
||||
percent = self._percent / 100
|
||||
return (delta * percent) + self._min_colortemp
|
||||
return self._min_colortemp
|
||||
|
||||
def _calc_brightness(self) -> float:
|
||||
if self._disable_brightness_adjust:
|
||||
return
|
||||
if self._is_sleep():
|
||||
return self._sleep_brightness
|
||||
if self._circadian_lighting._percent > 0:
|
||||
if self._percent > 0:
|
||||
return self._max_brightness
|
||||
delta_brightness = self._max_brightness - self._min_brightness
|
||||
percent = (100 + self._circadian_lighting._percent) / 100
|
||||
percent = (100 + self._percent) / 100
|
||||
return (delta_brightness * percent) + self._min_brightness
|
||||
|
||||
async def _update_switch(self, lights=None, transition=None, force=False):
|
||||
if self._only_once and not force:
|
||||
return
|
||||
self._hs_color = self._calc_hs()
|
||||
self._brightness = self._calc_brightness()
|
||||
await self.update()
|
||||
await self._adjust_lights(lights or self._lights, transition)
|
||||
|
||||
async def _force_update_switch(self, lights=None):
|
||||
|
|
@ -342,7 +573,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
|
|||
return
|
||||
|
||||
if transition is None:
|
||||
transition = self._circadian_lighting._transition
|
||||
transition = self._transition
|
||||
|
||||
tasks = []
|
||||
for light in lights:
|
||||
|
|
@ -355,12 +586,12 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
|
|||
|
||||
light_type = self._lights_types[light]
|
||||
if light_type == "ct":
|
||||
service_data[ATTR_COLOR_TEMP] = int(self._calc_ct())
|
||||
service_data[ATTR_COLOR_TEMP] = int(self._colortemp_mired)
|
||||
elif light_type == "rgb":
|
||||
r, g, b = self._calc_rgb()
|
||||
r, g, b = self._rgb_color
|
||||
service_data[ATTR_RGB_COLOR] = (int(r), int(g), int(b))
|
||||
elif light_type == "xy":
|
||||
service_data[ATTR_XY_COLOR] = self._calc_xy()
|
||||
service_data[ATTR_XY_COLOR] = self._xy_color
|
||||
if service_data.get(ATTR_BRIGHTNESS, False):
|
||||
service_data[ATTR_WHITE_VALUE] = service_data[ATTR_BRIGHTNESS]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue