use async functions and call trackers/listeners in the correct place

This commit is contained in:
Bas Nijholt 2020-09-01 18:39:16 +02:00
commit 4c9d1f6659
3 changed files with 93 additions and 97 deletions

View file

@ -29,11 +29,10 @@ Technical notes: I had to make a lot of assumptions when writing this app
import logging
from datetime import timedelta
import inspect
import voluptuous as vol
import astral
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.light import ATTR_TRANSITION, VALID_TRANSITION
from homeassistant.const import (
@ -44,9 +43,13 @@ from homeassistant.const import (
SUN_EVENT_SUNSET,
)
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import track_sunrise, track_sunset, track_time_change
from homeassistant.util import Throttle
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import (
async_track_sunrise,
async_track_sunset,
async_track_time_change,
async_track_time_interval,
)
from homeassistant.util.color import (
color_RGB_to_xy,
color_temperature_to_rgb,
@ -89,7 +92,7 @@ CONFIG_SCHEMA = vol.Schema(
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.positive_int,
vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period,
vol.Optional(
ATTR_TRANSITION, default=DEFAULT_TRANSITION
): VALID_TRANSITION,
@ -100,26 +103,6 @@ CONFIG_SCHEMA = vol.Schema(
)
def log(with_return=False, logger=_LOGGER):
def _log(func):
def wrapper(*args, **kwargs):
func_args = inspect.signature(func).bind(*args, **kwargs).arguments
key_value_pairs = (
f"{k}={v!r}" for k, v in func_args.items() if k != "self"
)
func_args_str = ", ".join(key_value_pairs)
out = f"{func.__qualname__}({func_args_str})"
result = func(*args, **kwargs)
if with_return:
out += f" -> {result}"
logger.debug(out)
return result
return wrapper
return _log
def setup(hass, config):
"""Set up the Circadian Lighting component."""
conf = config[DOMAIN]
@ -180,23 +163,30 @@ class CircadianLighting:
self._xy_color = self.calc_xy()
self._hs_color = self.calc_hs()
self.update = Throttle(timedelta(seconds=interval))(self._update)
if self._manual_time["sunrise"] is not None:
async_track_time_change(
self.hass,
self.update,
hour=self._manual_time["sunrise"].hour,
minute=self._manual_time["sunrise"].minute,
second=self._manual_time["sunrise"].second,
)
else:
async_track_sunrise(self.hass, self.update, self._sunrise_offset)
for which in ["sunrise", "sunrise"]:
time = self._manual_time[which]
if time is not None:
track_time_change(
self.hass,
self._update,
hour=time.hour,
minute=time.minute,
second=time.second,
)
if self._manual_time["sunset"] is not None:
async_track_time_change(
self.hass,
self.update,
hour=self._manual_time["sunset"].hour,
minute=self._manual_time["sunset"].minute,
second=self._manual_time["sunset"].second,
)
else:
async_track_sunset(self.hass, self.update, self._sunset_offset)
track_sunrise(self.hass, self._update, self._sunrise_offset)
track_sunset(self.hass, self._update, self._sunset_offset)
async_track_time_interval(self.hass, self.update, interval)
@log(with_return=True)
def get_timezone(self):
tf = TimezoneFinder()
timezone_string = tf.timezone_at(lng=self._longitude, lat=self._latitude)
@ -263,8 +253,8 @@ class CircadianLighting:
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
# 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))
today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET]
if (
@ -274,8 +264,8 @@ class CircadianLighting:
# Solar midnight is after sunset so use yesterdays's time
today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT]
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
# 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))
today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE]
if (
@ -335,12 +325,11 @@ class CircadianLighting:
def calc_hs(self):
return color_xy_to_hs(*self.calc_xy())
@log()
def _update(self):
async def update(self, _=None):
"""Update Circadian Values."""
self._percent = self.calc_percent()
self._colortemp = self.calc_colortemp()
self._rgb_color = self.calc_rgb()
self._xy_color = self.calc_xy()
self._hs_color = self.calc_hs()
dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC)
async_dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC)

View file

@ -2,11 +2,11 @@
Circadian Lighting Sensor for Home-Assistant.
"""
import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from custom_components.circadian_lighting import DOMAIN
from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN
ICON = "mdi:theme-light-dark"
@ -16,11 +16,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
circadian_lighting = hass.data.get(DOMAIN)
if circadian_lighting is not None:
sensor = CircadianSensor(hass, circadian_lighting)
add_devices([sensor])
add_devices([sensor], True)
def update(call=None):
"""Update component."""
circadian_lighting._update()
circadian_lighting.update()
service_name = "values_update"
hass.services.register(DOMAIN, service_name, update)
@ -78,9 +78,20 @@ class CircadianSensor(Entity):
"xy_color": self._circadian_lighting._xy_color,
}
def update(self):
"""Fetch new state data for the sensor.
@property
def should_poll(self) -> bool:
"""Disable polling."""
return False
This is the only method that should fetch new data for Home Assistant.
"""
self._circadian_lighting.update()
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
)
)
@callback
def _update_callback(self) -> None:
"""Triggers update of properties after receiving signal from CircadianLighting."""
self.async_schedule_update_ha_state(force_refresh=False)

View file

@ -2,10 +2,12 @@
Circadian Lighting Switch for Home-Assistant.
"""
import functools
import logging
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
@ -16,6 +18,7 @@ from homeassistant.components.light import (
)
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.light import VALID_TRANSITION, is_on
from homeassistant.components.switch import SwitchEntity
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_NAME,
@ -23,8 +26,8 @@ from homeassistant.const import (
SERVICE_TURN_ON,
STATE_ON,
)
from homeassistant.helpers.dispatcher import dispatcher_connect
from homeassistant.helpers.event import track_state_change
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 (
@ -34,17 +37,7 @@ from homeassistant.util.color import (
color_xy_to_hs,
)
from custom_components.circadian_lighting import (
CIRCADIAN_LIGHTING_UPDATE_TOPIC,
DOMAIN,
log,
)
try:
from homeassistant.components.switch import SwitchEntity
except ImportError:
from homeassistant.components.switch import SwitchDevice as SwitchEntity
from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -186,14 +179,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
self._lights_types[light] = "brightness"
self._lights = list(self._lights_types.keys())
# Register callbacks
dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_switch)
track_state_change(hass, self._lights, self.light_state_changed)
if self._sleep_entity is not None:
track_state_change(hass, self._sleep_entity, self.sleep_state_changed)
if self._disable_entity is not None:
track_state_change(hass, self._disable_entity, self.disable_state_changed)
@property
def entity_id(self):
"""Return the entity ID of the switch."""
@ -211,9 +196,33 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
async def async_added_to_hass(self):
"""Call when entity about to be added to hass."""
# If not None, we got an initial value.
await super().async_added_to_hass()
if self._state is not None:
# 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)
if self._sleep_entity is not None:
async_track_state_change(
self.hass, self._sleep_entity, self.sleep_state_changed
)
if self._disable_entity is not None:
disable_state_changed = functools.partial(
self._update_switch, transition=self._initial_transition, force=True
)
async_track_state_change(
self.hass,
self._disable_entity,
disable_state_changed,
from_state=self._disable_state,
)
if self._state is not None: # If not None, we got an initial value
return
state = await self.async_get_last_state()
@ -237,16 +246,13 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
"""Turn on circadian lighting."""
self._state = True
self._update_switch(transition=self._initial_transition, force=True)
self.schedule_update_ha_state()
def turn_off(self, **kwargs):
"""Turn off circadian lighting."""
self._state = False
self.schedule_update_ha_state()
self._hs_color = None
self._brightness = None
@log(with_return=True, logger=_LOGGER)
def is_sleep(self):
return (
self._sleep_entity is not None
@ -283,7 +289,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
percent = (100 + self._circadian_lighting._percent) / 100
return (delta_brightness * percent) + self._min_brightness
@log(logger=_LOGGER)
def _update_switch(self, lights=None, transition=None, force=False):
if self._only_once and not force:
return
@ -291,14 +296,12 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
self._brightness = self.calc_brightness()
self._adjust_lights(lights or self._lights, transition)
@log(with_return=True, logger=_LOGGER)
def _is_disabled(self):
return (
self._disable_entity is not None
and self.hass.states.get(self._disable_entity).state in self._disable_state
)
@log(with_return=True, logger=_LOGGER)
def _should_adjust(self):
if self._state is not True:
return False
@ -306,7 +309,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
return False
return True
def _adjust_lights(self, lights, transition=None):
def _adjust_lights(self, lights, transition):
if not self._should_adjust():
return
@ -337,17 +340,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity):
self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data)
_LOGGER.debug(f"{light} {light_type} Adjusted - {service_data}")
@log(with_return=True, logger=_LOGGER)
def light_state_changed(self, entity_id, from_state, to_state):
if to_state.state == "on" and from_state.state != "on":
self._update_switch([entity_id], self._initial_transition, force=True)
@log(with_return=True, logger=_LOGGER)
def sleep_state_changed(self, entity_id, from_state, to_state):
if to_state.state in self._sleep_state or from_state.state in self._sleep_state:
self._update_switch(transition=self._initial_transition, force=True)
@log(with_return=True, logger=_LOGGER)
def disable_state_changed(self, entity_id, from_state, to_state):
if from_state.state in self._disable_state:
self._update_switch(transition=self._initial_transition, force=True)