Compare commits

...

14 commits

Author SHA1 Message Date
Bas Nijholt
ef444cd941 rename _calc_percent 2020-09-21 12:32:25 +02:00
Bas Nijholt
8abab027cf Merge branch 'different-today' into light-profile 2020-09-21 12:31:38 +02:00
Bas Nijholt
2eea22b6da construct 'today' dict differently 2020-09-19 11:19:09 +02:00
Bas Nijholt
e31915c20b switch doesn't need to have the profile as attribute 2020-09-10 11:58:32 +02:00
Bas Nijholt
32dea3aa51 setup a sensor per profile 2020-09-10 11:58:32 +02:00
Bas Nijholt
31da53182f fix doc-string 2020-09-10 11:58:32 +02:00
Bas Nijholt
5663a7b1a9 make switch use profile 2020-09-10 11:58:32 +02:00
Bas Nijholt
4234be19ec fix import 2020-09-10 11:58:32 +02:00
Bas Nijholt
db78ab2027 setup sensor for default profile 2020-09-10 11:58:32 +02:00
Bas Nijholt
90cbd2980e profiles -> profile 2020-09-10 11:58:32 +02:00
Bas Nijholt
fd1f2fdb66 add option to add multiple profiles 2020-09-10 11:58:32 +02:00
Bas Nijholt
f6219de84f another attempt 2020-09-10 11:58:32 +02:00
Bas Nijholt
eb8da2e46b initial implementation of profiles 2020-09-10 11:58:32 +02:00
Bas Nijholt
65b1aa4b84 start implementing profile settings, as suggested in https://github.com/claytonjn/hass-circadian_lighting/issues/91#issuecomment-647084406 2020-09-10 11:58:31 +02:00
3 changed files with 105 additions and 109 deletions

View file

@ -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()

View file

@ -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

View file

@ -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):