mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-12 14:54:04 +02:00
Add auto_reset_manual_control with async timer
This commit is contained in:
parent
1ca3bf0f6a
commit
52275bfee3
3 changed files with 126 additions and 3 deletions
|
|
@ -73,6 +73,8 @@ CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0
|
|||
TURNING_OFF_DELAY = 5
|
||||
CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0
|
||||
|
||||
CONF_AUTORESET_CONTROL, DEFAULT_AUTORESET_CONTROL = "autoreset_control_time", 0
|
||||
|
||||
|
||||
def int_between(min_int, max_int):
|
||||
"""Return an integer between 'min_int' and 'max_int'."""
|
||||
|
|
@ -121,6 +123,11 @@ VALIDATION_TUPLES = [
|
|||
(CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool),
|
||||
(CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)),
|
||||
(CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float),
|
||||
(
|
||||
CONF_AUTORESET_CONTROL,
|
||||
DEFAULT_AUTORESET_CONTROL,
|
||||
int_between(0, 7 * 24 * 60 * 60), # 7 days max
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ from .const import (
|
|||
ATTR_ADAPT_COLOR,
|
||||
ATTR_TURN_ON_OFF_LISTENER,
|
||||
CONF_ADAPT_DELAY,
|
||||
CONF_AUTORESET_CONTROL,
|
||||
CONF_DETECT_NON_HA_CHANGES,
|
||||
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES,
|
||||
CONF_INITIAL_TRANSITION,
|
||||
|
|
@ -496,7 +497,7 @@ async def async_setup_entry(
|
|||
all_lights = _expand_light_groups(this_switch.hass, lights)
|
||||
if service_call.data[CONF_MANUAL_CONTROL]:
|
||||
for light in all_lights:
|
||||
this_switch.turn_on_off_listener.manual_control[light] = True
|
||||
this_switch.turn_on_off_listener.mark_as_manual_control(light)
|
||||
_fire_manual_control_event(this_switch, light, service_call.context)
|
||||
else:
|
||||
this_switch.turn_on_off_listener.reset(*all_lights)
|
||||
|
|
@ -843,6 +844,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self._transition = data[CONF_TRANSITION]
|
||||
self._adapt_delay = data[CONF_ADAPT_DELAY]
|
||||
self._send_split_delay = data[CONF_SEND_SPLIT_DELAY]
|
||||
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
|
||||
_loc = get_astral_location(self.hass)
|
||||
if isinstance(_loc, tuple):
|
||||
# Astral v2.2
|
||||
|
|
@ -916,6 +918,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
def _expand_light_groups(self) -> None:
|
||||
all_lights = _expand_light_groups(self.hass, self._lights)
|
||||
self.turn_on_off_listener.lights.update(all_lights)
|
||||
self.turn_on_off_listener.set_auto_reset_manual_control_times(
|
||||
all_lights, self._auto_reset_manual_control_time
|
||||
)
|
||||
self._lights = list(all_lights)
|
||||
|
||||
async def _setup_listeners(self, _=None) -> None:
|
||||
|
|
@ -1549,6 +1554,10 @@ class TurnOnOffListener:
|
|||
# Track last 'service_data' to 'light.turn_on' resulting from this integration
|
||||
self.last_service_data: dict[str, dict[str, Any]] = {}
|
||||
|
||||
# Track auto reset of manual_control
|
||||
self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {}
|
||||
self.auto_reset_manual_control_times: dict[str, float | None] = {}
|
||||
|
||||
# When a state is different `max_cnt_significant_changes` times in a row,
|
||||
# mark it as manually_controlled.
|
||||
self.max_cnt_significant_changes = 2
|
||||
|
|
@ -1560,11 +1569,55 @@ class TurnOnOffListener:
|
|||
EVENT_STATE_CHANGED, self.state_changed_event_listener
|
||||
)
|
||||
|
||||
def set_auto_reset_manual_control_times(self, lights, time):
|
||||
"""Set the time after which the lights are automatically reset."""
|
||||
for light in lights:
|
||||
if (old_time := self.auto_reset_manual_control_times.get(light)) and (
|
||||
old_time != time
|
||||
):
|
||||
_LOGGER.info(
|
||||
"Setting auto_reset_manual_control for '%s' from %s seconds to %s seconds."
|
||||
" This might happen because the light is in multiple swiches.",
|
||||
light,
|
||||
old_time,
|
||||
time,
|
||||
)
|
||||
self.auto_reset_manual_control_times[light] = time
|
||||
|
||||
def mark_as_manual_control(self, light: str) -> None:
|
||||
"""Mark a light as manually controlled."""
|
||||
self.manual_control[light] = True
|
||||
delay = self.auto_reset_manual_control_times.get(light)
|
||||
if timer := self.auto_reset_manual_control_timers.get(light):
|
||||
if delay is None: # Timer object exists, but should not anymore
|
||||
timer.cancel()
|
||||
self.auto_reset_manual_control_timers.pop(light)
|
||||
else: # Timer object already exists, just update the delay and restart it
|
||||
timer.delay = delay
|
||||
timer.start()
|
||||
elif delay is not None: # Timer object does not exist, create it
|
||||
|
||||
def reset():
|
||||
self.reset(light)
|
||||
_LOGGER.debug(
|
||||
"Auto resetting 'manual_control' status of '%s' because"
|
||||
" it was not manually controlled for %s seconds.",
|
||||
light,
|
||||
delay,
|
||||
)
|
||||
|
||||
timer = _AsyncSingleShotTimer(delay, reset)
|
||||
self.auto_reset_manual_control_timers[light] = timer
|
||||
timer.start()
|
||||
|
||||
def reset(self, *lights, reset_manual_control=True) -> None:
|
||||
"""Reset the 'manual_control' status of the lights."""
|
||||
for light in lights:
|
||||
if reset_manual_control:
|
||||
self.manual_control[light] = False
|
||||
timer = self.auto_reset_manual_control_timers.pop(light, None)
|
||||
if timer:
|
||||
timer.cancel()
|
||||
self.last_state_change.pop(light, None)
|
||||
self.last_service_data.pop(light, None)
|
||||
self.cnt_significant_changes[light] = 0
|
||||
|
|
@ -1697,7 +1750,7 @@ class TurnOnOffListener:
|
|||
):
|
||||
# Light was already on and 'light.turn_on' was not called by
|
||||
# the adaptive_lighting integration.
|
||||
manual_control = self.manual_control[light] = True
|
||||
manual_control = self.mark_as_manual_control(light)
|
||||
_fire_manual_control_event(switch, light, turn_on_event.context)
|
||||
_LOGGER.debug(
|
||||
"'%s' was already on and 'light.turn_on' was not called by the"
|
||||
|
|
@ -1769,7 +1822,7 @@ class TurnOnOffListener:
|
|||
# Only mark a light as significantly changing, if changed==True
|
||||
# N times in a row. We do this because sometimes a state changes
|
||||
# happens only *after* a new update interval has already started.
|
||||
self.manual_control[light] = True
|
||||
self.mark_as_manual_control(light)
|
||||
_fire_manual_control_event(switch, light, context, is_async=False)
|
||||
else:
|
||||
if n_changes > 1:
|
||||
|
|
@ -1880,3 +1933,33 @@ class TurnOnOffListener:
|
|||
# other 'off' → 'on' state switches resulting from polling. That
|
||||
# would mean we 'return True' here.
|
||||
return False
|
||||
|
||||
|
||||
class _AsyncSingleShotTimer:
|
||||
def __init__(self, delay, callback):
|
||||
"""Initialize the timer."""
|
||||
self.delay = delay
|
||||
self.callback = callback
|
||||
self.task = None
|
||||
|
||||
async def _run(self):
|
||||
"""Run the timer. Don't call this directly, use start() instead."""
|
||||
await asyncio.sleep(self.delay)
|
||||
if self.callback:
|
||||
if asyncio.iscoroutinefunction(self.callback):
|
||||
await self.callback()
|
||||
else:
|
||||
self.callback()
|
||||
|
||||
def start(self):
|
||||
"""Start the timer."""
|
||||
if self.task is not None and not self.task.done():
|
||||
self.task.cancel()
|
||||
|
||||
self.task = asyncio.create_task(self._run())
|
||||
|
||||
def cancel(self):
|
||||
"""Cancel the timer."""
|
||||
if self.task:
|
||||
self.task.cancel()
|
||||
self.callback = None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue