diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 6cf2ec92..5b11e768 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -29,7 +29,7 @@ CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" -CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True +CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", False CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index e89e0b5a..f9a82c98 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -38,7 +38,7 @@ "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, (NOT YET IMPLEMENTED!) if manually adjusting the lights when they are already on", + "take_over_control": "take_over_control, (beta feature) if manually adjusting the lights when they are already on", "transition": "transition, in seconds" } } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1e5587c5..358bb777 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -269,7 +269,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Locks that prevent light adjusting when waiting for a light to 'turn_off' self._locks: Dict[str, asyncio.Lock] = {} # To identify that this integration made a change - self._context = Context() + self.__context = Context() # self._context will be overwritten # Set in self._update_attrs_and_maybe_adapt_lights self._light_settings = {} @@ -370,6 +370,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return {key: None for key in self._light_settings} return self._light_settings + def _reset_take_over_control(self): + for light in self._lights: + self.turn_on_off_listener.manually_controlled[light] = False + async def async_turn_on( # pylint: disable=arguments-differ self, adapt_lights: bool = True ) -> None: @@ -380,6 +384,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.is_on: return self._state = True + self._reset_take_over_control() await self._setup_listeners() if adapt_lights: await self._update_attrs_and_maybe_adapt_lights( @@ -392,6 +397,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._state = False self._remove_listeners() + self._reset_take_over_control() async def _async_update_at_interval(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights(force=False) @@ -450,7 +456,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, - context=self._context, + context=self.__context, ) async def _update_attrs_and_maybe_adapt_lights( @@ -469,23 +475,32 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): lights = self._lights if (self._only_once and not force) or not lights: return - await self._adapt_lights(lights, transition) + await self._adapt_lights(lights, transition, force) async def _adapt_lights( - self, lights: List[str], transition: Optional[int] + self, lights: List[str], transition: Optional[int], force: bool ): _LOGGER.debug( - "%s: '_adapt_lights(%s, %s)' called", self.name, lights, transition + "%s: '_adapt_lights(%s, %s, %s)' called", + self.name, + lights, + transition, + force, ) for light in lights: if not is_on(self.hass, light): continue if self._take_over_control: - if await self.turn_on_off_listener.is_manually_adjusted( + if self.turn_on_off_listener.is_manually_controlled( light, - off_to_on_event=self._off_to_on_event.get(light), - adaptive_lighting_context=self._context, + force, + adaptive_lighting_context=self.__context, ): + _LOGGER.debug( + "%s: '%s' is being manually controlled, stop adapting.", + self._name, + light, + ) continue await self._adapt_light(light, transition) @@ -493,6 +508,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() await self._update_attrs_and_maybe_adapt_lights( transition=self._initial_transition, force=True ) @@ -540,6 +556,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): # Tracks 'off' → 'on' state changes self._on_to_off_event[entity_id] = event + self.turn_on_off_listener.manually_controlled[entity_id] = False class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): @@ -752,6 +769,8 @@ class TurnOnOffListener: self.turn_on_event: Dict[str, Event] = {} # Keeps 'asyncio.sleep` tasks that can be cancelled by 'light.turn_on' events self.sleep_tasks: Dict[str, asyncio.Task] = {} + # Tracks which lights are manually controlled + self.manually_controlled: Dict[str, bool] = {} self.remove_listener = self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener @@ -782,6 +801,7 @@ class TurnOnOffListener: ) for eid in entity_ids: self.turn_off_event[eid] = event + self.manually_controlled[eid] = False elif service == SERVICE_TURN_ON: _LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_ids) @@ -791,12 +811,35 @@ class TurnOnOffListener: task.cancel() self.turn_on_event[eid] = event - async def is_manually_adjusted(self, light: str, off_to_on_event: Optional[Event], adaptive_lighting_context: Context): + def is_manually_controlled( + self, + light: str, + force: bool, + adaptive_lighting_context: Context, + ): """Check if the light has been 'on' and is now manually being adjusted.""" - if off_to_on_event is None: - # No state change has been registered before, so we can't tell. - return False - return False + manually_controlled = self.manually_controlled[light] + if manually_controlled: + # Manually controlled until light is turned on and off + return True + + turn_on_event = self.turn_on_event.get(light) + if ( + turn_on_event is not None + and adaptive_lighting_context.id != turn_on_event.context.id + and not force + ): + # Light was already on and 'light.turn_on' was not called by + # the adaptive_lighting integration. + manually_controlled = self.manually_controlled[light] = True + _LOGGER.debug( + "'%s' was already on and 'light.turn_on' was not called by the" + " adaptive_lighting integration, the Adaptive Lighting will stop" + " adapting the light until the switch or the light turns off and" + " then on again.", + light, + ) + return manually_controlled async def maybe_cancel_adjusting( self, entity_id: str, off_to_on_event: Event, on_to_off_event: Optional[Event] diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index e89e0b5a..f9a82c98 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -38,7 +38,7 @@ "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, (NOT YET IMPLEMENTED!) if manually adjusting the lights when they are already on", + "take_over_control": "take_over_control, (beta feature) if manually adjusting the lights when they are already on", "transition": "transition, in seconds" } }