mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-11 22:34: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
|
||||
"""
|
||||
|
||||
import bisect
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
|
|
@ -72,52 +73,66 @@ CONF_SUNSET_OFFSET = "sunset_offset"
|
|||
CONF_SUNRISE_TIME = "sunrise_time"
|
||||
CONF_SUNSET_TIME = "sunset_time"
|
||||
DEFAULT_TRANSITION = 60
|
||||
CONF_PROFILE, DEFAULT_PROFILE = "profile", "default"
|
||||
|
||||
_DOMAIN_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1000, max=10000)
|
||||
),
|
||||
vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1000, max=10000)
|
||||
),
|
||||
vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str,
|
||||
vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str,
|
||||
vol.Optional(CONF_SUNRISE_TIME): cv.time,
|
||||
vol.Optional(CONF_SUNSET_TIME): cv.time,
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_ELEVATION): float,
|
||||
vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period,
|
||||
vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION,
|
||||
vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _all_unique_profiles(value):
|
||||
"""Validate that all enties have a unique profile name."""
|
||||
hosts = [device[CONF_PROFILE] for device in value]
|
||||
schema = vol.Schema(vol.Unique())
|
||||
schema(hosts)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1000, max=10000)
|
||||
),
|
||||
vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1000, max=10000)
|
||||
),
|
||||
vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str,
|
||||
vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str,
|
||||
vol.Optional(CONF_SUNRISE_TIME): cv.time,
|
||||
vol.Optional(CONF_SUNSET_TIME): cv.time,
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_ELEVATION): float,
|
||||
vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period,
|
||||
vol.Optional(
|
||||
ATTR_TRANSITION, default=DEFAULT_TRANSITION
|
||||
): VALID_TRANSITION,
|
||||
}
|
||||
),
|
||||
},
|
||||
{DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Circadian Lighting platform."""
|
||||
conf = config[DOMAIN]
|
||||
hass.data[DOMAIN] = CircadianLighting(
|
||||
hass,
|
||||
min_colortemp=conf.get(CONF_MIN_CT),
|
||||
max_colortemp=conf.get(CONF_MAX_CT),
|
||||
sunrise_offset=conf.get(CONF_SUNRISE_OFFSET),
|
||||
sunset_offset=conf.get(CONF_SUNSET_OFFSET),
|
||||
sunrise_time=conf.get(CONF_SUNRISE_TIME),
|
||||
sunset_time=conf.get(CONF_SUNSET_TIME),
|
||||
latitude=conf.get(CONF_LATITUDE, hass.config.latitude),
|
||||
longitude=conf.get(CONF_LONGITUDE, hass.config.longitude),
|
||||
elevation=conf.get(CONF_ELEVATION, hass.config.elevation),
|
||||
interval=conf.get(CONF_INTERVAL),
|
||||
transition=conf.get(ATTR_TRANSITION),
|
||||
)
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
configs = config[DOMAIN]
|
||||
for conf in configs:
|
||||
profile = conf[CONF_PROFILE]
|
||||
hass.data[DOMAIN][profile] = CircadianLighting(
|
||||
hass,
|
||||
min_colortemp=conf[CONF_MIN_CT],
|
||||
max_colortemp=conf[CONF_MAX_CT],
|
||||
sunrise_offset=conf.get(CONF_SUNRISE_OFFSET),
|
||||
sunset_offset=conf.get(CONF_SUNSET_OFFSET),
|
||||
sunrise_time=conf.get(CONF_SUNRISE_TIME),
|
||||
sunset_time=conf.get(CONF_SUNSET_TIME),
|
||||
latitude=conf.get(CONF_LATITUDE, hass.config.latitude),
|
||||
longitude=conf.get(CONF_LONGITUDE, hass.config.longitude),
|
||||
elevation=conf.get(CONF_ELEVATION, hass.config.elevation),
|
||||
interval=conf[CONF_INTERVAL],
|
||||
transition=conf[ATTR_TRANSITION],
|
||||
profile=profile,
|
||||
)
|
||||
load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
|
@ -140,6 +155,7 @@ class CircadianLighting:
|
|||
elevation,
|
||||
interval,
|
||||
transition,
|
||||
profile,
|
||||
):
|
||||
self.hass = hass
|
||||
self._min_colortemp = min_colortemp
|
||||
|
|
@ -152,8 +168,10 @@ class CircadianLighting:
|
|||
self._longitude = longitude
|
||||
self._elevation = elevation
|
||||
self._transition = transition
|
||||
self._profile = profile
|
||||
_LOGGER.debug("profile: %s", self._profile)
|
||||
|
||||
self._percent = self.calc_percent()
|
||||
self._percent = self._calc_percent()
|
||||
self._colortemp = self.calc_colortemp()
|
||||
self._rgb_color = self.calc_rgb()
|
||||
self._xy_color = self.calc_xy()
|
||||
|
|
@ -192,7 +210,7 @@ class CircadianLighting:
|
|||
microsecond=other_date.microsecond,
|
||||
)
|
||||
|
||||
def get_sunrise_sunset(self, date):
|
||||
def _get_sun_events(self, date):
|
||||
if self._manual_sunrise is not None and self._manual_sunset is not None:
|
||||
sunrise = self._replace_time(date, "sunrise")
|
||||
sunset = self._replace_time(date, "sunset")
|
||||
|
|
@ -235,34 +253,19 @@ class CircadianLighting:
|
|||
k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items()
|
||||
}
|
||||
|
||||
def calc_percent(self):
|
||||
def _relevant_events(self, now):
|
||||
events = []
|
||||
for days in [-1, 0, 1]:
|
||||
sun_events = self._get_sun_events(now + timedelta(days=days))
|
||||
events.extend(list(sun_events.items()))
|
||||
events = sorted(events, key=lambda x: x[1])
|
||||
index_now = bisect.bisect([ts for _, ts in events], now.timestamp())
|
||||
return dict(events[index_now - 2 : index_now + 2])
|
||||
|
||||
def _calc_percent(self):
|
||||
now = dt_util.utcnow()
|
||||
now_ts = now.timestamp()
|
||||
|
||||
today = self.get_sunrise_sunset(now)
|
||||
if now_ts < today[SUN_EVENT_SUNRISE]:
|
||||
# It's before sunrise (after midnight), because it's before
|
||||
# sunrise (and after midnight) sunset must have happend yesterday.
|
||||
yesterday = self.get_sunrise_sunset(now - timedelta(days=1))
|
||||
if (
|
||||
today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET]
|
||||
and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET]
|
||||
):
|
||||
# Solar midnight is after sunset so use yesterdays's time
|
||||
today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT]
|
||||
today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET]
|
||||
elif now_ts > today[SUN_EVENT_SUNSET]:
|
||||
# It's after sunset (before midnight), because it's after sunset
|
||||
# (and before midnight) sunrise should happen tomorrow.
|
||||
tomorrow = self.get_sunrise_sunset(now + timedelta(days=1))
|
||||
if (
|
||||
today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE]
|
||||
and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE]
|
||||
):
|
||||
# Solar midnight is before sunrise so use tomorrow's time
|
||||
today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT]
|
||||
today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE]
|
||||
|
||||
today = self._relevant_events(now)
|
||||
# Figure out where we are in time so we know which half of the
|
||||
# parabola to calculate. We're generating a different
|
||||
# sunset-sunrise parabola for before and after solar midnight.
|
||||
|
|
@ -315,7 +318,7 @@ class CircadianLighting:
|
|||
|
||||
async def update(self, _=None):
|
||||
"""Update Circadian Values."""
|
||||
self._percent = self.calc_percent()
|
||||
self._percent = self._calc_percent()
|
||||
self._colortemp = self.calc_colortemp()
|
||||
self._rgb_color = self.calc_rgb()
|
||||
self._xy_color = self.calc_xy()
|
||||
|
|
|
|||
|
|
@ -6,27 +6,19 @@ from homeassistant.core import callback
|
|||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN
|
||||
from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DEFAULT_PROFILE, DOMAIN
|
||||
|
||||
ICON = "mdi:theme-light-dark"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Circadian Lighting sensor."""
|
||||
circadian_lighting = hass.data.get(DOMAIN)
|
||||
if circadian_lighting is not None:
|
||||
sensor = CircadianSensor(hass, circadian_lighting)
|
||||
add_devices([sensor], True)
|
||||
|
||||
def update(call=None):
|
||||
"""Update component."""
|
||||
circadian_lighting.update()
|
||||
|
||||
service_name = "values_update"
|
||||
hass.services.register(DOMAIN, service_name, update)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
sensors = [
|
||||
CircadianSensor(hass, circadian_lighting)
|
||||
for circadian_lighting in hass.data[DOMAIN].values()
|
||||
]
|
||||
add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
class CircadianSensor(Entity):
|
||||
|
|
@ -37,6 +29,10 @@ class CircadianSensor(Entity):
|
|||
self._circadian_lighting = circadian_lighting
|
||||
self._name = "Circadian Values"
|
||||
self._entity_id = "sensor.circadian_values"
|
||||
profile = circadian_lighting._profile
|
||||
if profile != DEFAULT_PROFILE:
|
||||
self._name += f" {profile}"
|
||||
self._entity_id += f"_{profile.lower()}"
|
||||
self._unit_of_measurement = "%"
|
||||
self._icon = ICON
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ from homeassistant.util.color import (
|
|||
color_xy_to_hs,
|
||||
)
|
||||
|
||||
from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN
|
||||
from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN, CONF_PROFILE, DEFAULT_PROFILE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -89,39 +89,36 @@ PLATFORM_SCHEMA = vol.Schema(
|
|||
CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION
|
||||
): VALID_TRANSITION,
|
||||
vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Circadian Lighting switches."""
|
||||
circadian_lighting = hass.data.get(DOMAIN)
|
||||
if circadian_lighting is not None:
|
||||
switch = CircadianSwitch(
|
||||
hass,
|
||||
circadian_lighting,
|
||||
name=config.get(CONF_NAME),
|
||||
lights_ct=config.get(CONF_LIGHTS_CT, []),
|
||||
lights_rgb=config.get(CONF_LIGHTS_RGB, []),
|
||||
lights_xy=config.get(CONF_LIGHTS_XY, []),
|
||||
lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []),
|
||||
disable_brightness_adjust=config.get(CONF_DISABLE_BRIGHTNESS_ADJUST),
|
||||
min_brightness=config.get(CONF_MIN_BRIGHT),
|
||||
max_brightness=config.get(CONF_MAX_BRIGHT),
|
||||
sleep_entity=config.get(CONF_SLEEP_ENTITY),
|
||||
sleep_state=config.get(CONF_SLEEP_STATE),
|
||||
sleep_colortemp=config.get(CONF_SLEEP_CT),
|
||||
sleep_brightness=config.get(CONF_SLEEP_BRIGHT),
|
||||
disable_entity=config.get(CONF_DISABLE_ENTITY),
|
||||
disable_state=config.get(CONF_DISABLE_STATE),
|
||||
initial_transition=config.get(CONF_INITIAL_TRANSITION),
|
||||
only_once=config.get(CONF_ONLY_ONCE),
|
||||
)
|
||||
add_devices([switch])
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
profile = config[CONF_PROFILE]
|
||||
circadian_lighting = hass.data[DOMAIN][profile]
|
||||
switch = CircadianSwitch(
|
||||
hass,
|
||||
circadian_lighting,
|
||||
name=config[CONF_NAME],
|
||||
lights_ct=config.get(CONF_LIGHTS_CT, []),
|
||||
lights_rgb=config.get(CONF_LIGHTS_RGB, []),
|
||||
lights_xy=config.get(CONF_LIGHTS_XY, []),
|
||||
lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []),
|
||||
disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST],
|
||||
min_brightness=config[CONF_MIN_BRIGHT],
|
||||
max_brightness=config[CONF_MAX_BRIGHT],
|
||||
sleep_entity=config.get(CONF_SLEEP_ENTITY),
|
||||
sleep_state=config.get(CONF_SLEEP_STATE),
|
||||
sleep_colortemp=config[CONF_SLEEP_CT],
|
||||
sleep_brightness=config[CONF_SLEEP_BRIGHT],
|
||||
disable_entity=config.get(CONF_DISABLE_ENTITY),
|
||||
disable_state=config.get(CONF_DISABLE_STATE),
|
||||
initial_transition=config[CONF_INITIAL_TRANSITION],
|
||||
only_once=config[CONF_ONLY_ONCE],
|
||||
)
|
||||
add_devices([switch])
|
||||
|
||||
|
||||
def _difference_between_states(from_state, to_state):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue