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

View file

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

View file

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