mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-14 07:44:04 +02:00
Compare commits
14 commits
main
...
light-prof
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef444cd941 | ||
|
|
8abab027cf | ||
|
|
2eea22b6da | ||
|
|
e31915c20b | ||
|
|
32dea3aa51 | ||
|
|
31da53182f | ||
|
|
5663a7b1a9 | ||
|
|
4234be19ec | ||
|
|
db78ab2027 | ||
|
|
90cbd2980e | ||
|
|
fd1f2fdb66 | ||
|
|
f6219de84f | ||
|
|
eb8da2e46b | ||
|
|
65b1aa4b84 |
3 changed files with 105 additions and 109 deletions
|
|
@ -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
|
lights to 2700K (warm white) until your hub goes into Night mode
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import bisect
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
@ -72,52 +73,66 @@ CONF_SUNSET_OFFSET = "sunset_offset"
|
||||||
CONF_SUNRISE_TIME = "sunrise_time"
|
CONF_SUNRISE_TIME = "sunrise_time"
|
||||||
CONF_SUNSET_TIME = "sunset_time"
|
CONF_SUNSET_TIME = "sunset_time"
|
||||||
DEFAULT_TRANSITION = 60
|
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(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)},
|
||||||
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,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up the Circadian Lighting platform."""
|
"""Set up the Circadian Lighting platform."""
|
||||||
conf = config[DOMAIN]
|
if DOMAIN not in hass.data:
|
||||||
hass.data[DOMAIN] = CircadianLighting(
|
hass.data[DOMAIN] = {}
|
||||||
hass,
|
configs = config[DOMAIN]
|
||||||
min_colortemp=conf.get(CONF_MIN_CT),
|
for conf in configs:
|
||||||
max_colortemp=conf.get(CONF_MAX_CT),
|
profile = conf[CONF_PROFILE]
|
||||||
sunrise_offset=conf.get(CONF_SUNRISE_OFFSET),
|
hass.data[DOMAIN][profile] = CircadianLighting(
|
||||||
sunset_offset=conf.get(CONF_SUNSET_OFFSET),
|
hass,
|
||||||
sunrise_time=conf.get(CONF_SUNRISE_TIME),
|
min_colortemp=conf[CONF_MIN_CT],
|
||||||
sunset_time=conf.get(CONF_SUNSET_TIME),
|
max_colortemp=conf[CONF_MAX_CT],
|
||||||
latitude=conf.get(CONF_LATITUDE, hass.config.latitude),
|
sunrise_offset=conf.get(CONF_SUNRISE_OFFSET),
|
||||||
longitude=conf.get(CONF_LONGITUDE, hass.config.longitude),
|
sunset_offset=conf.get(CONF_SUNSET_OFFSET),
|
||||||
elevation=conf.get(CONF_ELEVATION, hass.config.elevation),
|
sunrise_time=conf.get(CONF_SUNRISE_TIME),
|
||||||
interval=conf.get(CONF_INTERVAL),
|
sunset_time=conf.get(CONF_SUNSET_TIME),
|
||||||
transition=conf.get(ATTR_TRANSITION),
|
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)
|
load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
@ -140,6 +155,7 @@ class CircadianLighting:
|
||||||
elevation,
|
elevation,
|
||||||
interval,
|
interval,
|
||||||
transition,
|
transition,
|
||||||
|
profile,
|
||||||
):
|
):
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._min_colortemp = min_colortemp
|
self._min_colortemp = min_colortemp
|
||||||
|
|
@ -152,8 +168,10 @@ class CircadianLighting:
|
||||||
self._longitude = longitude
|
self._longitude = longitude
|
||||||
self._elevation = elevation
|
self._elevation = elevation
|
||||||
self._transition = transition
|
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._colortemp = self.calc_colortemp()
|
||||||
self._rgb_color = self.calc_rgb()
|
self._rgb_color = self.calc_rgb()
|
||||||
self._xy_color = self.calc_xy()
|
self._xy_color = self.calc_xy()
|
||||||
|
|
@ -192,7 +210,7 @@ class CircadianLighting:
|
||||||
microsecond=other_date.microsecond,
|
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:
|
if self._manual_sunrise is not None and self._manual_sunset is not None:
|
||||||
sunrise = self._replace_time(date, "sunrise")
|
sunrise = self._replace_time(date, "sunrise")
|
||||||
sunset = self._replace_time(date, "sunset")
|
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()
|
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 = dt_util.utcnow()
|
||||||
now_ts = now.timestamp()
|
now_ts = now.timestamp()
|
||||||
|
today = self._relevant_events(now)
|
||||||
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
|
# Figure out where we are in time so we know which half of the
|
||||||
# parabola to calculate. We're generating a different
|
# parabola to calculate. We're generating a different
|
||||||
# sunset-sunrise parabola for before and after solar midnight.
|
# sunset-sunrise parabola for before and after solar midnight.
|
||||||
|
|
@ -315,7 +318,7 @@ class CircadianLighting:
|
||||||
|
|
||||||
async def update(self, _=None):
|
async def update(self, _=None):
|
||||||
"""Update Circadian Values."""
|
"""Update Circadian Values."""
|
||||||
self._percent = self.calc_percent()
|
self._percent = self._calc_percent()
|
||||||
self._colortemp = self.calc_colortemp()
|
self._colortemp = self.calc_colortemp()
|
||||||
self._rgb_color = self.calc_rgb()
|
self._rgb_color = self.calc_rgb()
|
||||||
self._xy_color = self.calc_xy()
|
self._xy_color = self.calc_xy()
|
||||||
|
|
|
||||||
|
|
@ -6,27 +6,19 @@ from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
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"
|
ICON = "mdi:theme-light-dark"
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the Circadian Lighting sensor."""
|
"""Set up the Circadian Lighting sensor."""
|
||||||
circadian_lighting = hass.data.get(DOMAIN)
|
sensors = [
|
||||||
if circadian_lighting is not None:
|
CircadianSensor(hass, circadian_lighting)
|
||||||
sensor = CircadianSensor(hass, circadian_lighting)
|
for circadian_lighting in hass.data[DOMAIN].values()
|
||||||
add_devices([sensor], True)
|
]
|
||||||
|
add_devices(sensors, True)
|
||||||
def update(call=None):
|
return True
|
||||||
"""Update component."""
|
|
||||||
circadian_lighting.update()
|
|
||||||
|
|
||||||
service_name = "values_update"
|
|
||||||
hass.services.register(DOMAIN, service_name, update)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class CircadianSensor(Entity):
|
class CircadianSensor(Entity):
|
||||||
|
|
@ -37,6 +29,10 @@ class CircadianSensor(Entity):
|
||||||
self._circadian_lighting = circadian_lighting
|
self._circadian_lighting = circadian_lighting
|
||||||
self._name = "Circadian Values"
|
self._name = "Circadian Values"
|
||||||
self._entity_id = "sensor.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._unit_of_measurement = "%"
|
||||||
self._icon = ICON
|
self._icon = ICON
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ from homeassistant.util.color import (
|
||||||
color_xy_to_hs,
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -89,39 +89,36 @@ PLATFORM_SCHEMA = vol.Schema(
|
||||||
CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION
|
CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION
|
||||||
): VALID_TRANSITION,
|
): VALID_TRANSITION,
|
||||||
vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean,
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the Circadian Lighting switches."""
|
"""Set up the Circadian Lighting switches."""
|
||||||
circadian_lighting = hass.data.get(DOMAIN)
|
profile = config[CONF_PROFILE]
|
||||||
if circadian_lighting is not None:
|
circadian_lighting = hass.data[DOMAIN][profile]
|
||||||
switch = CircadianSwitch(
|
switch = CircadianSwitch(
|
||||||
hass,
|
hass,
|
||||||
circadian_lighting,
|
circadian_lighting,
|
||||||
name=config.get(CONF_NAME),
|
name=config[CONF_NAME],
|
||||||
lights_ct=config.get(CONF_LIGHTS_CT, []),
|
lights_ct=config.get(CONF_LIGHTS_CT, []),
|
||||||
lights_rgb=config.get(CONF_LIGHTS_RGB, []),
|
lights_rgb=config.get(CONF_LIGHTS_RGB, []),
|
||||||
lights_xy=config.get(CONF_LIGHTS_XY, []),
|
lights_xy=config.get(CONF_LIGHTS_XY, []),
|
||||||
lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []),
|
lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []),
|
||||||
disable_brightness_adjust=config.get(CONF_DISABLE_BRIGHTNESS_ADJUST),
|
disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST],
|
||||||
min_brightness=config.get(CONF_MIN_BRIGHT),
|
min_brightness=config[CONF_MIN_BRIGHT],
|
||||||
max_brightness=config.get(CONF_MAX_BRIGHT),
|
max_brightness=config[CONF_MAX_BRIGHT],
|
||||||
sleep_entity=config.get(CONF_SLEEP_ENTITY),
|
sleep_entity=config.get(CONF_SLEEP_ENTITY),
|
||||||
sleep_state=config.get(CONF_SLEEP_STATE),
|
sleep_state=config.get(CONF_SLEEP_STATE),
|
||||||
sleep_colortemp=config.get(CONF_SLEEP_CT),
|
sleep_colortemp=config[CONF_SLEEP_CT],
|
||||||
sleep_brightness=config.get(CONF_SLEEP_BRIGHT),
|
sleep_brightness=config[CONF_SLEEP_BRIGHT],
|
||||||
disable_entity=config.get(CONF_DISABLE_ENTITY),
|
disable_entity=config.get(CONF_DISABLE_ENTITY),
|
||||||
disable_state=config.get(CONF_DISABLE_STATE),
|
disable_state=config.get(CONF_DISABLE_STATE),
|
||||||
initial_transition=config.get(CONF_INITIAL_TRANSITION),
|
initial_transition=config[CONF_INITIAL_TRANSITION],
|
||||||
only_once=config.get(CONF_ONLY_ONCE),
|
only_once=config[CONF_ONLY_ONCE],
|
||||||
)
|
)
|
||||||
add_devices([switch])
|
add_devices([switch])
|
||||||
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _difference_between_states(from_state, to_state):
|
def _difference_between_states(from_state, to_state):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue