diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index bb75cc07..575d7cd7 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -14,6 +14,7 @@ from .const import ( # pylint: disable=unused-import NONE_STR, VALIDATION_TUPLES, ) +from .switch import _supported_features _LOGGER = logging.getLogger(__name__) @@ -89,10 +90,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if not errors: return self.async_create_entry(title="", data=user_input) - all_lights = sorted(self.hass.states.async_entity_ids("light")) - to_replace = { - CONF_LIGHTS: cv.multi_select(all_lights), - } + all_lights = [ + light + for light in self.hass.states.async_entity_ids("light") + if _supported_features(self.hass, light) + ] + to_replace = {CONF_LIGHTS: cv.multi_select(sorted(all_lights))} options_schema = {} for name, default, validation in VALIDATION_TUPLES: diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index d0ad5ce9..d08cbc19 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -38,8 +38,8 @@ "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", "sunset_time": "sunset_time, in 'HH:MM:SS' format", - "take_over_control": "take_over_control, if manually adjusting the lights when they are already on", - "detect_non_ha_changes": "detect_non_ha_changes, detect changes to lights made outside of HA (calls 'homeassistant.update_entity'!)", + "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "transition, in seconds" } } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6867a59e..0ce86683 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -218,9 +218,12 @@ def _supported_features(hass, light: str): return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} -def abs_rel_diff(a, b): +def abs_rel_diff(val_a, val_b): """Absolute relative difference in %.""" - return abs((a - b) / b) * 100 + if val_b == 0: + # To avoid ZeroDivisionError + val_b = 1e-6 + return abs((val_a - val_b) / val_b) * 100 class AdaptiveSwitch(SwitchEntity, RestoreEntity): @@ -382,9 +385,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return {key: None for key in self._settings} return self._settings - def _reset_take_over_control(self): - self.turn_on_off_listener.reset(*self._lights) - async def async_turn_on( # pylint: disable=arguments-differ self, adapt_lights: bool = True ) -> None: @@ -395,7 +395,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.is_on: return self._state = True - self._reset_take_over_control() + self.turn_on_off_listener.reset(*self._lights) await self._setup_listeners() if adapt_lights: await self._update_attrs_and_maybe_adapt_lights( @@ -408,7 +408,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._state = False self._remove_listeners() - self._reset_take_over_control() + self.turn_on_off_listener.reset(*self._lights) async def _async_update_at_interval(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights(force=False) @@ -467,6 +467,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._adapt_brightness, self._adapt_color_temp, self._adapt_rgb_color, + self.__context, ) ): return @@ -534,7 +535,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not match_state_event(event, ("on", "off")): return _LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event) - self._reset_take_over_control() + self.turn_on_off_listener.reset(*self._lights) await self._update_attrs_and_maybe_adapt_lights( transition=self._initial_transition, force=True ) @@ -552,6 +553,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug( "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id ) + self.turn_on_off_listener.reset(entity_id) # Tracks 'off' → 'on' state changes self._off_to_on_event[entity_id] = event lock = self._locks.get(entity_id) @@ -867,7 +869,13 @@ class TurnOnOffListener: return manually_controlled async def significant_change( - self, light, adapt_brightness, adapt_color_temp, adapt_rgb_color, threshold=5 + self, + light, + adapt_brightness, + adapt_color_temp, + adapt_rgb_color, + context, + threshold=5, ): """Has the light made a significant change since last update. @@ -885,6 +893,7 @@ class TurnOnOffListener: SERVICE_UPDATE_ENTITY, {ATTR_ENTITY_ID: light}, blocking=True, + context=context, ) attributes = self.hass.states.get(light).attributes if ( @@ -895,7 +904,12 @@ class TurnOnOffListener: applied_brightness = round(255 * service_data[ATTR_BRIGHTNESS_PCT] / 100) current_brightness = attributes["brightness"] if abs_rel_diff(current_brightness, applied_brightness) > threshold: - _LOGGER.debug("Brightness of '%s' significantly changed", light) + _LOGGER.debug( + "Brightness of '%s' significantly changed from %s to %s", + light, + applied_brightness, + current_brightness, + ) changed = True if ( @@ -907,8 +921,10 @@ class TurnOnOffListener: current_color_temp = attributes[ATTR_COLOR_TEMP] if abs_rel_diff(current_color_temp, applied_color_temp) > threshold: _LOGGER.debug( - "Color temperature of '%s' significantly changed", + "Color temperature of '%s' significantly changed from %s to %s", light, + applied_color_temp, + current_color_temp, ) changed = True @@ -922,8 +938,10 @@ class TurnOnOffListener: for col_applied, col_current in zip(applied_rgb_color, current_rgb_color): if abs_rel_diff(col_applied, col_current) > threshold: _LOGGER.debug( - "color RGB of '%s' significantly changed", + "color RGB of '%s' significantly changed from %s to %s", light, + applied_rgb_color, + current_rgb_color, ) changed = True break @@ -962,8 +980,7 @@ class TurnOnOffListener: id_on_to_off = on_to_off_event.context.id turn_off_event = self.turn_off_event.get(entity_id) - id_turn_off = turn_off_event.context.id - transition = turn_off_event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) + transition = turn_off_event.data.get(ATTR_SERVICE_DATA, {}).get(ATTR_TRANSITION) turn_on_event = self.turn_on_event.get(entity_id) id_turn_on = turn_on_event.context.id @@ -975,7 +992,8 @@ class TurnOnOffListener: return False if ( - id_on_to_off == id_turn_off + turn_off_event is not None + and id_on_to_off == turn_off_event.context.id and id_on_to_off is not None and transition is not None # 'turn_off' is called with transition=... ): diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index d0ad5ce9..d08cbc19 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -38,8 +38,8 @@ "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", "sunset_time": "sunset_time, in 'HH:MM:SS' format", - "take_over_control": "take_over_control, if manually adjusting the lights when they are already on", - "detect_non_ha_changes": "detect_non_ha_changes, detect changes to lights made outside of HA (calls 'homeassistant.update_entity'!)", + "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "transition, in seconds" } }