Improve debounce cooldown (#32161)
This commit is contained in:
parent
853d6cda25
commit
2a88ae559e
2 changed files with 67 additions and 12 deletions
|
@ -31,6 +31,7 @@ class Debouncer:
|
||||||
self.immediate = immediate
|
self.immediate = immediate
|
||||||
self._timer_task: Optional[asyncio.TimerHandle] = None
|
self._timer_task: Optional[asyncio.TimerHandle] = None
|
||||||
self._execute_at_end_of_timer: bool = False
|
self._execute_at_end_of_timer: bool = False
|
||||||
|
self._execute_lock = asyncio.Lock()
|
||||||
|
|
||||||
async def async_call(self) -> None:
|
async def async_call(self) -> None:
|
||||||
"""Call the function."""
|
"""Call the function."""
|
||||||
|
@ -42,15 +43,23 @@ class Debouncer:
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.immediate:
|
# Locked means a call is in progress. Any call is good, so abort.
|
||||||
await self.hass.async_add_job(self.function) # type: ignore
|
if self._execute_lock.locked():
|
||||||
else:
|
return
|
||||||
self._execute_at_end_of_timer = True
|
|
||||||
|
|
||||||
self._timer_task = self.hass.loop.call_later(
|
if not self.immediate:
|
||||||
self.cooldown,
|
self._execute_at_end_of_timer = True
|
||||||
lambda: self.hass.async_create_task(self._handle_timer_finish()),
|
self._schedule_timer()
|
||||||
)
|
return
|
||||||
|
|
||||||
|
async with self._execute_lock:
|
||||||
|
# Abort if timer got set while we're waiting for the lock.
|
||||||
|
if self._timer_task:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.hass.async_add_job(self.function) # type: ignore
|
||||||
|
|
||||||
|
self._schedule_timer()
|
||||||
|
|
||||||
async def _handle_timer_finish(self) -> None:
|
async def _handle_timer_finish(self) -> None:
|
||||||
"""Handle a finished timer."""
|
"""Handle a finished timer."""
|
||||||
|
@ -63,11 +72,22 @@ class Debouncer:
|
||||||
|
|
||||||
self._execute_at_end_of_timer = False
|
self._execute_at_end_of_timer = False
|
||||||
|
|
||||||
|
# Locked means a call is in progress. Any call is good, so abort.
|
||||||
|
if self._execute_lock.locked():
|
||||||
|
return
|
||||||
|
|
||||||
|
async with self._execute_lock:
|
||||||
|
# Abort if timer got set while we're waiting for the lock.
|
||||||
|
if self._timer_task:
|
||||||
|
return # type: ignore
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.hass.async_add_job(self.function) # type: ignore
|
await self.hass.async_add_job(self.function) # type: ignore
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
self.logger.exception("Unexpected exception from %s", self.function)
|
self.logger.exception("Unexpected exception from %s", self.function)
|
||||||
|
|
||||||
|
self._schedule_timer()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_cancel(self) -> None:
|
def async_cancel(self) -> None:
|
||||||
"""Cancel any scheduled call."""
|
"""Cancel any scheduled call."""
|
||||||
|
@ -76,3 +96,11 @@ class Debouncer:
|
||||||
self._timer_task = None
|
self._timer_task = None
|
||||||
|
|
||||||
self._execute_at_end_of_timer = False
|
self._execute_at_end_of_timer = False
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _schedule_timer(self) -> None:
|
||||||
|
"""Schedule a timer."""
|
||||||
|
self._timer_task = self.hass.loop.call_later(
|
||||||
|
self.cooldown,
|
||||||
|
lambda: self.hass.async_create_task(self._handle_timer_finish()),
|
||||||
|
)
|
||||||
|
|
|
@ -15,20 +15,24 @@ async def test_immediate_works(hass):
|
||||||
function=CoroutineMock(side_effect=lambda: calls.append(None)),
|
function=CoroutineMock(side_effect=lambda: calls.append(None)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Call when nothing happening
|
||||||
await debouncer.async_call()
|
await debouncer.async_call()
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
assert debouncer._timer_task is not None
|
assert debouncer._timer_task is not None
|
||||||
assert debouncer._execute_at_end_of_timer is False
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
|
||||||
|
# Call when cooldown active setting execute at end to True
|
||||||
await debouncer.async_call()
|
await debouncer.async_call()
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
assert debouncer._timer_task is not None
|
assert debouncer._timer_task is not None
|
||||||
assert debouncer._execute_at_end_of_timer is True
|
assert debouncer._execute_at_end_of_timer is True
|
||||||
|
|
||||||
|
# Canceling debounce in cooldown
|
||||||
debouncer.async_cancel()
|
debouncer.async_cancel()
|
||||||
assert debouncer._timer_task is None
|
assert debouncer._timer_task is None
|
||||||
assert debouncer._execute_at_end_of_timer is False
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
|
||||||
|
# Call and let timer run out
|
||||||
await debouncer.async_call()
|
await debouncer.async_call()
|
||||||
assert len(calls) == 2
|
assert len(calls) == 2
|
||||||
await debouncer._handle_timer_finish()
|
await debouncer._handle_timer_finish()
|
||||||
|
@ -36,6 +40,14 @@ async def test_immediate_works(hass):
|
||||||
assert debouncer._timer_task is None
|
assert debouncer._timer_task is None
|
||||||
assert debouncer._execute_at_end_of_timer is False
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
|
||||||
|
# Test calling doesn't execute/cooldown if currently executing.
|
||||||
|
await debouncer._execute_lock.acquire()
|
||||||
|
await debouncer.async_call()
|
||||||
|
assert len(calls) == 2
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
debouncer._execute_lock.release()
|
||||||
|
|
||||||
|
|
||||||
async def test_not_immediate_works(hass):
|
async def test_not_immediate_works(hass):
|
||||||
"""Test immediate works."""
|
"""Test immediate works."""
|
||||||
|
@ -48,23 +60,38 @@ async def test_not_immediate_works(hass):
|
||||||
function=CoroutineMock(side_effect=lambda: calls.append(None)),
|
function=CoroutineMock(side_effect=lambda: calls.append(None)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Call when nothing happening
|
||||||
await debouncer.async_call()
|
await debouncer.async_call()
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
assert debouncer._timer_task is not None
|
assert debouncer._timer_task is not None
|
||||||
assert debouncer._execute_at_end_of_timer is True
|
assert debouncer._execute_at_end_of_timer is True
|
||||||
|
|
||||||
|
# Call while still on cooldown
|
||||||
await debouncer.async_call()
|
await debouncer.async_call()
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
assert debouncer._timer_task is not None
|
assert debouncer._timer_task is not None
|
||||||
assert debouncer._execute_at_end_of_timer is True
|
assert debouncer._execute_at_end_of_timer is True
|
||||||
|
|
||||||
|
# Canceling while on cooldown
|
||||||
debouncer.async_cancel()
|
debouncer.async_cancel()
|
||||||
assert debouncer._timer_task is None
|
assert debouncer._timer_task is None
|
||||||
assert debouncer._execute_at_end_of_timer is False
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
|
||||||
|
# Call and let timer run out
|
||||||
await debouncer.async_call()
|
await debouncer.async_call()
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
await debouncer._handle_timer_finish()
|
await debouncer._handle_timer_finish()
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
assert debouncer._timer_task is not None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
|
||||||
|
# Reset debouncer
|
||||||
|
debouncer.async_cancel()
|
||||||
|
|
||||||
|
# Test calling doesn't schedule if currently executing.
|
||||||
|
await debouncer._execute_lock.acquire()
|
||||||
|
await debouncer.async_call()
|
||||||
|
assert len(calls) == 1
|
||||||
assert debouncer._timer_task is None
|
assert debouncer._timer_task is None
|
||||||
assert debouncer._execute_at_end_of_timer is False
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
debouncer._execute_lock.release()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue