mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
Add async_schedule_call to the Debouncer (#111051)
This commit is contained in:
parent
aaa071e810
commit
490c03d248
@ -969,7 +969,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
|
|||||||
self._hass_config = hass_config
|
self._hass_config = hass_config
|
||||||
self._pending_import_flows: dict[str, dict[str, asyncio.Future[None]]] = {}
|
self._pending_import_flows: dict[str, dict[str, asyncio.Future[None]]] = {}
|
||||||
self._initialize_futures: dict[str, list[asyncio.Future[None]]] = {}
|
self._initialize_futures: dict[str, list[asyncio.Future[None]]] = {}
|
||||||
self._discovery_debouncer = Debouncer(
|
self._discovery_debouncer = Debouncer[None](
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
cooldown=DISCOVERY_COOLDOWN,
|
cooldown=DISCOVERY_COOLDOWN,
|
||||||
|
@ -61,26 +61,44 @@ class Debouncer(Generic[_R_co]):
|
|||||||
f"debouncer cooldown={self.cooldown}, immediate={self.immediate}",
|
f"debouncer cooldown={self.cooldown}, immediate={self.immediate}",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_call(self) -> None:
|
@callback
|
||||||
"""Call the function."""
|
def async_schedule_call(self) -> None:
|
||||||
|
"""Schedule a call to the function."""
|
||||||
|
if self._async_schedule_or_call_now():
|
||||||
|
self._execute_at_end_of_timer = True
|
||||||
|
self._on_debounce()
|
||||||
|
|
||||||
|
def _async_schedule_or_call_now(self) -> bool:
|
||||||
|
"""Check if a call should be scheduled.
|
||||||
|
|
||||||
|
Returns True if the function should be called immediately.
|
||||||
|
|
||||||
|
Returns False if there is nothing to do.
|
||||||
|
"""
|
||||||
if self._shutdown_requested:
|
if self._shutdown_requested:
|
||||||
self.logger.debug("Debouncer call ignored as shutdown has been requested.")
|
self.logger.debug("Debouncer call ignored as shutdown has been requested.")
|
||||||
return
|
return False
|
||||||
assert self._job is not None
|
|
||||||
|
|
||||||
if self._timer_task:
|
if self._timer_task:
|
||||||
if not self._execute_at_end_of_timer:
|
if not self._execute_at_end_of_timer:
|
||||||
self._execute_at_end_of_timer = True
|
self._execute_at_end_of_timer = True
|
||||||
|
|
||||||
return
|
return False
|
||||||
|
|
||||||
# Locked means a call is in progress. Any call is good, so abort.
|
# Locked means a call is in progress. Any call is good, so abort.
|
||||||
if self._execute_lock.locked():
|
if self._execute_lock.locked():
|
||||||
return
|
return False
|
||||||
|
|
||||||
if not self.immediate:
|
if not self.immediate:
|
||||||
self._execute_at_end_of_timer = True
|
self._execute_at_end_of_timer = True
|
||||||
self._schedule_timer()
|
self._schedule_timer()
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def async_call(self) -> None:
|
||||||
|
"""Call the function."""
|
||||||
|
if not self._async_schedule_or_call_now():
|
||||||
return
|
return
|
||||||
|
|
||||||
async with self._execute_lock:
|
async with self._execute_lock:
|
||||||
@ -88,6 +106,7 @@ class Debouncer(Generic[_R_co]):
|
|||||||
if self._timer_task:
|
if self._timer_task:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
assert self._job is not None
|
||||||
try:
|
try:
|
||||||
if task := self.hass.async_run_hass_job(self._job):
|
if task := self.hass.async_run_hass_job(self._job):
|
||||||
await task
|
await task
|
||||||
@ -137,6 +156,7 @@ class Debouncer(Generic[_R_co]):
|
|||||||
"""Create job task, but only if pending."""
|
"""Create job task, but only if pending."""
|
||||||
self._timer_task = None
|
self._timer_task = None
|
||||||
if self._execute_at_end_of_timer:
|
if self._execute_at_end_of_timer:
|
||||||
|
self._execute_at_end_of_timer = False
|
||||||
self.hass.async_create_task(
|
self.hass.async_create_task(
|
||||||
self._handle_timer_finish(),
|
self._handle_timer_finish(),
|
||||||
f"debouncer {self._job} finish cooldown={self.cooldown}, immediate={self.immediate}",
|
f"debouncer {self._job} finish cooldown={self.cooldown}, immediate={self.immediate}",
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -69,6 +69,106 @@ async def test_immediate_works(hass: HomeAssistant) -> None:
|
|||||||
assert debouncer._job.target == debouncer.function
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
|
||||||
|
async def test_immediate_works_with_schedule_call(hass: HomeAssistant) -> None:
|
||||||
|
"""Test immediate works with scheduled calls."""
|
||||||
|
calls = []
|
||||||
|
debouncer = debounce.Debouncer(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
cooldown=0.01,
|
||||||
|
immediate=True,
|
||||||
|
function=AsyncMock(side_effect=lambda: calls.append(None)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call when nothing happening
|
||||||
|
debouncer.async_schedule_call()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert debouncer._timer_task is not None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
# Call when cooldown active setting execute at end to True
|
||||||
|
debouncer.async_schedule_call()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert debouncer._timer_task is not None
|
||||||
|
assert debouncer._execute_at_end_of_timer is True
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
# Canceling debounce in cooldown
|
||||||
|
debouncer.async_cancel()
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
before_job = debouncer._job
|
||||||
|
|
||||||
|
# Call and let timer run out
|
||||||
|
debouncer.async_schedule_call()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 2
|
||||||
|
async_fire_time_changed(hass, utcnow() + timedelta(seconds=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 2
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
assert debouncer._job == before_job
|
||||||
|
|
||||||
|
# Test calling doesn't execute/cooldown if currently executing.
|
||||||
|
await debouncer._execute_lock.acquire()
|
||||||
|
debouncer.async_schedule_call()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 2
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
debouncer._execute_lock.release()
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
|
||||||
|
async def test_immediate_works_with_callback_function(hass: HomeAssistant) -> None:
|
||||||
|
"""Test immediate works with callback function."""
|
||||||
|
calls = []
|
||||||
|
debouncer = debounce.Debouncer(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
cooldown=0.01,
|
||||||
|
immediate=True,
|
||||||
|
function=callback(Mock(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
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
debouncer.async_cancel()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_immediate_works_with_executor_function(hass: HomeAssistant) -> None:
|
||||||
|
"""Test immediate works with executor function."""
|
||||||
|
calls = []
|
||||||
|
debouncer = debounce.Debouncer(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
cooldown=0.01,
|
||||||
|
immediate=True,
|
||||||
|
function=Mock(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
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
debouncer.async_cancel()
|
||||||
|
|
||||||
|
|
||||||
async def test_immediate_works_with_passed_callback_function_raises(
|
async def test_immediate_works_with_passed_callback_function_raises(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -247,6 +347,61 @@ async def test_not_immediate_works(hass: HomeAssistant) -> None:
|
|||||||
assert debouncer._job.target == debouncer.function
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
|
||||||
|
async def test_not_immediate_works_schedule_call(hass: HomeAssistant) -> None:
|
||||||
|
"""Test immediate works with schedule call."""
|
||||||
|
calls = []
|
||||||
|
debouncer = debounce.Debouncer(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
cooldown=0.01,
|
||||||
|
immediate=False,
|
||||||
|
function=AsyncMock(side_effect=lambda: calls.append(None)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call when nothing happening
|
||||||
|
debouncer.async_schedule_call()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
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
|
||||||
|
debouncer.async_schedule_call()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
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
|
||||||
|
debouncer.async_schedule_call()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 0
|
||||||
|
async_fire_time_changed(hass, utcnow() + timedelta(seconds=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert debouncer._timer_task is not None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
# Reset debouncer
|
||||||
|
debouncer.async_cancel()
|
||||||
|
|
||||||
|
# Test calling doesn't schedule if currently executing.
|
||||||
|
await debouncer._execute_lock.acquire()
|
||||||
|
debouncer.async_schedule_call()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert debouncer._timer_task is None
|
||||||
|
assert debouncer._execute_at_end_of_timer is False
|
||||||
|
debouncer._execute_lock.release()
|
||||||
|
assert debouncer._job.target == debouncer.function
|
||||||
|
|
||||||
|
|
||||||
async def test_immediate_works_with_function_swapped(hass: HomeAssistant) -> None:
|
async def test_immediate_works_with_function_swapped(hass: HomeAssistant) -> None:
|
||||||
"""Test immediate works and we can change out the function."""
|
"""Test immediate works and we can change out the function."""
|
||||||
calls = []
|
calls = []
|
||||||
|
Loading…
x
Reference in New Issue
Block a user