mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Improve debounce cooldown (#32161)
This commit is contained in:
parent
853d6cda25
commit
2a88ae559e
@ -31,6 +31,7 @@ class Debouncer:
|
||||
self.immediate = immediate
|
||||
self._timer_task: Optional[asyncio.TimerHandle] = None
|
||||
self._execute_at_end_of_timer: bool = False
|
||||
self._execute_lock = asyncio.Lock()
|
||||
|
||||
async def async_call(self) -> None:
|
||||
"""Call the function."""
|
||||
@ -42,15 +43,23 @@ class Debouncer:
|
||||
|
||||
return
|
||||
|
||||
if self.immediate:
|
||||
await self.hass.async_add_job(self.function) # type: ignore
|
||||
else:
|
||||
self._execute_at_end_of_timer = True
|
||||
# Locked means a call is in progress. Any call is good, so abort.
|
||||
if self._execute_lock.locked():
|
||||
return
|
||||
|
||||
self._timer_task = self.hass.loop.call_later(
|
||||
self.cooldown,
|
||||
lambda: self.hass.async_create_task(self._handle_timer_finish()),
|
||||
)
|
||||
if not self.immediate:
|
||||
self._execute_at_end_of_timer = True
|
||||
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:
|
||||
"""Handle a finished timer."""
|
||||
@ -63,10 +72,21 @@ class Debouncer:
|
||||
|
||||
self._execute_at_end_of_timer = False
|
||||
|
||||
try:
|
||||
await self.hass.async_add_job(self.function) # type: ignore
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.logger.exception("Unexpected exception from %s", self.function)
|
||||
# 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:
|
||||
await self.hass.async_add_job(self.function) # type: ignore
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.logger.exception("Unexpected exception from %s", self.function)
|
||||
|
||||
self._schedule_timer()
|
||||
|
||||
@callback
|
||||
def async_cancel(self) -> None:
|
||||
@ -76,3 +96,11 @@ class Debouncer:
|
||||
self._timer_task = None
|
||||
|
||||
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)),
|
||||
)
|
||||
|
||||
# Call when nothing happening
|
||||
await debouncer.async_call()
|
||||
assert len(calls) == 1
|
||||
assert debouncer._timer_task is not None
|
||||
assert debouncer._execute_at_end_of_timer is False
|
||||
|
||||
# Call when cooldown active setting execute at end to True
|
||||
await debouncer.async_call()
|
||||
assert len(calls) == 1
|
||||
assert debouncer._timer_task is not None
|
||||
assert debouncer._execute_at_end_of_timer is True
|
||||
|
||||
# Canceling debounce in cooldown
|
||||
debouncer.async_cancel()
|
||||
assert debouncer._timer_task is None
|
||||
assert debouncer._execute_at_end_of_timer is False
|
||||
|
||||
# Call and let timer run out
|
||||
await debouncer.async_call()
|
||||
assert len(calls) == 2
|
||||
await debouncer._handle_timer_finish()
|
||||
@ -36,6 +40,14 @@ async def test_immediate_works(hass):
|
||||
assert debouncer._timer_task is None
|
||||
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):
|
||||
"""Test immediate works."""
|
||||
@ -48,23 +60,38 @@ async def test_not_immediate_works(hass):
|
||||
function=CoroutineMock(side_effect=lambda: calls.append(None)),
|
||||
)
|
||||
|
||||
# Call when nothing happening
|
||||
await debouncer.async_call()
|
||||
assert len(calls) == 0
|
||||
assert debouncer._timer_task is not None
|
||||
assert debouncer._execute_at_end_of_timer is True
|
||||
|
||||
# Call while still on cooldown
|
||||
await debouncer.async_call()
|
||||
assert len(calls) == 0
|
||||
assert debouncer._timer_task is not None
|
||||
assert debouncer._execute_at_end_of_timer is True
|
||||
|
||||
# Canceling while on cooldown
|
||||
debouncer.async_cancel()
|
||||
assert debouncer._timer_task is None
|
||||
assert debouncer._execute_at_end_of_timer is False
|
||||
|
||||
# Call and let timer run out
|
||||
await debouncer.async_call()
|
||||
assert len(calls) == 0
|
||||
await debouncer._handle_timer_finish()
|
||||
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._execute_at_end_of_timer is False
|
||||
debouncer._execute_lock.release()
|
||||
|
Loading…
x
Reference in New Issue
Block a user