From 4c9d1f66598f4ec0d47565cc7aed84b207afe4da Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 18:39:16 +0200 Subject: [PATCH] use async functions and call trackers/listeners in the correct place --- .../circadian_lighting/__init__.py | 85 ++++++++----------- .../circadian_lighting/sensor.py | 31 ++++--- .../circadian_lighting/switch.py | 74 ++++++++-------- 3 files changed, 93 insertions(+), 97 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 6de46c6f..281d9afa 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -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) diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 4df3cc1e..b924fa3f 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -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) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 16a38e97..584d0785 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -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)