Home Assistant Unofficial Reference 2024.12.1
debounce.py
Go to the documentation of this file.
1 """Debounce helper."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Callable
7 from logging import Logger
8 
9 from homeassistant.core import HassJob, HomeAssistant, callback
10 
11 
12 class Debouncer[_R_co]:
13  """Class to rate limit calls to a specific command."""
14 
15  def __init__(
16  self,
17  hass: HomeAssistant,
18  logger: Logger,
19  *,
20  cooldown: float,
21  immediate: bool,
22  function: Callable[[], _R_co] | None = None,
23  background: bool = False,
24  ) -> None:
25  """Initialize debounce.
26 
27  immediate: indicate if the function needs to be called right away and
28  wait <cooldown> until executing next invocation.
29  function: optional and can be instantiated later.
30  """
31  self.hasshass = hass
32  self.loggerlogger = logger
33  self._function_function = function
34  self.cooldowncooldown = cooldown
35  self.immediateimmediate = immediate
36  self._timer_task_timer_task: asyncio.TimerHandle | None = None
37  self._execute_at_end_of_timer_execute_at_end_of_timer: bool = False
38  self._execute_lock_execute_lock = asyncio.Lock()
39  self._background_background = background
40  self._job_job: HassJob[[], _R_co] | None = (
41  None
42  if function is None
43  else HassJob(
44  function, f"debouncer cooldown={cooldown}, immediate={immediate}"
45  )
46  )
47  self._shutdown_requested_shutdown_requested = False
48 
49  @property
50  def function(self) -> Callable[[], _R_co] | None:
51  """Return the function being wrapped by the Debouncer."""
52  return self._function_function
53 
54  @function.setter
55  def function(self, function: Callable[[], _R_co]) -> None:
56  """Update the function being wrapped by the Debouncer."""
57  self._function_function = function
58  if self._job_job is None or function != self._job_job.target:
59  self._job_job = HassJob(
60  function,
61  f"debouncer cooldown={self.cooldown}, immediate={self.immediate}",
62  )
63 
64  @callback
65  def async_schedule_call(self) -> None:
66  """Schedule a call to the function."""
67  if self._async_schedule_or_call_now_async_schedule_or_call_now():
68  self._execute_at_end_of_timer_execute_at_end_of_timer = True
69  self._on_debounce_on_debounce()
70 
71  def _async_schedule_or_call_now(self) -> bool:
72  """Check if a call should be scheduled.
73 
74  Returns True if the function should be called immediately.
75 
76  Returns False if there is nothing to do.
77  """
78  if self._shutdown_requested_shutdown_requested:
79  self.loggerlogger.debug("Debouncer call ignored as shutdown has been requested.")
80  return False
81 
82  if self._timer_task_timer_task:
83  if not self._execute_at_end_of_timer_execute_at_end_of_timer:
84  self._execute_at_end_of_timer_execute_at_end_of_timer = True
85 
86  return False
87 
88  # Locked means a call is in progress. Any call is good, so abort.
89  if self._execute_lock_execute_lock.locked():
90  return False
91 
92  if not self.immediateimmediate:
93  self._execute_at_end_of_timer_execute_at_end_of_timer = True
94  self._schedule_timer_schedule_timer()
95  return False
96 
97  return True
98 
99  async def async_call(self) -> None:
100  """Call the function."""
101  if not self._async_schedule_or_call_now_async_schedule_or_call_now():
102  return
103 
104  async with self._execute_lock_execute_lock:
105  # Abort if timer got set while we're waiting for the lock.
106  if self._timer_task_timer_task:
107  return
108 
109  assert self._job_job is not None
110  try:
111  if task := self.hasshass.async_run_hass_job(
112  self._job_job, background=self._background_background
113  ):
114  await task
115  finally:
116  self._schedule_timer_schedule_timer()
117 
118  async def _handle_timer_finish(self) -> None:
119  """Handle a finished timer."""
120  assert self._job_job is not None
121 
122  self._execute_at_end_of_timer_execute_at_end_of_timer = False
123 
124  # Locked means a call is in progress. Any call is good, so abort.
125  if self._execute_lock_execute_lock.locked():
126  return
127 
128  async with self._execute_lock_execute_lock:
129  # Abort if timer got set while we're waiting for the lock.
130  if self._timer_task_timer_task:
131  return
132 
133  try:
134  if task := self.hasshass.async_run_hass_job(
135  self._job_job, background=self._background_background
136  ):
137  await task
138  except Exception:
139  self.loggerlogger.exception("Unexpected exception from %s", self.functionfunctionfunction)
140  finally:
141  # Schedule a new timer to prevent new runs during cooldown
142  self._schedule_timer_schedule_timer()
143 
144  @callback
145  def async_shutdown(self) -> None:
146  """Cancel any scheduled call, and prevent new runs."""
147  self._shutdown_requested_shutdown_requested = True
148  self.async_cancelasync_cancel()
149 
150  @callback
151  def async_cancel(self) -> None:
152  """Cancel any scheduled call."""
153  if self._timer_task_timer_task:
154  self._timer_task_timer_task.cancel()
155  self._timer_task_timer_task = None
156 
157  self._execute_at_end_of_timer_execute_at_end_of_timer = False
158 
159  @callback
160  def _on_debounce(self) -> None:
161  """Create job task, but only if pending."""
162  self._timer_task_timer_task = None
163  if not self._execute_at_end_of_timer_execute_at_end_of_timer:
164  return
165  self._execute_at_end_of_timer_execute_at_end_of_timer = False
166  name = f"debouncer {self._job} finish cooldown={self.cooldown}, immediate={self.immediate}"
167  if not self._background_background:
168  self.hasshass.async_create_task(
169  self._handle_timer_finish_handle_timer_finish(), name, eager_start=True
170  )
171  return
172  self.hasshass.async_create_background_task(
173  self._handle_timer_finish_handle_timer_finish(), name, eager_start=True
174  )
175 
176  @callback
177  def _schedule_timer(self) -> None:
178  """Schedule a timer."""
179  if not self._shutdown_requested_shutdown_requested:
180  self._timer_task_timer_task = self.hasshass.loop.call_later(
181  self.cooldowncooldown, self._on_debounce_on_debounce
182  )
Callable[[], _R_co]|None function(self)
Definition: debounce.py:50
None __init__(self, HomeAssistant hass, Logger logger, *float cooldown, bool immediate, Callable[[], _R_co]|None function=None, bool background=False)
Definition: debounce.py:24
None function(self, Callable[[], _R_co] function)
Definition: debounce.py:55