Improve debounce cooldown (#32161)

This commit is contained in:
Paulus Schoutsen 2020-02-26 11:27:37 -08:00 committed by GitHub
parent 853d6cda25
commit 2a88ae559e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 67 additions and 12 deletions

View file

@ -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()),
)

View file

@ -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()