2020-01-31 22:47:40 +00:00
|
|
|
"""Debounce helper."""
|
2021-03-17 17:34:19 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2020-01-31 22:47:40 +00:00
|
|
|
import asyncio
|
2021-09-29 14:32:11 +00:00
|
|
|
from collections.abc import Awaitable, Callable
|
2020-01-31 22:47:40 +00:00
|
|
|
from logging import Logger
|
2021-09-29 14:32:11 +00:00
|
|
|
from typing import Any
|
2020-01-31 22:47:40 +00:00
|
|
|
|
2020-10-09 06:40:36 +00:00
|
|
|
from homeassistant.core import HassJob, HomeAssistant, callback
|
2020-01-31 22:47:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Debouncer:
|
|
|
|
"""Class to rate limit calls to a specific command."""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
hass: HomeAssistant,
|
|
|
|
logger: Logger,
|
2020-02-06 17:29:29 +00:00
|
|
|
*,
|
2020-01-31 22:47:40 +00:00
|
|
|
cooldown: float,
|
|
|
|
immediate: bool,
|
2021-03-17 17:34:19 +00:00
|
|
|
function: Callable[..., Awaitable[Any]] | None = None,
|
2021-05-20 15:53:29 +00:00
|
|
|
) -> None:
|
2020-01-31 22:47:40 +00:00
|
|
|
"""Initialize debounce.
|
|
|
|
|
|
|
|
immediate: indicate if the function needs to be called right away and
|
2020-05-05 17:53:46 +00:00
|
|
|
wait <cooldown> until executing next invocation.
|
2020-01-31 22:47:40 +00:00
|
|
|
function: optional and can be instantiated later.
|
|
|
|
"""
|
|
|
|
self.hass = hass
|
|
|
|
self.logger = logger
|
2020-10-09 06:40:36 +00:00
|
|
|
self._function = function
|
2020-01-31 22:47:40 +00:00
|
|
|
self.cooldown = cooldown
|
|
|
|
self.immediate = immediate
|
2021-03-17 17:34:19 +00:00
|
|
|
self._timer_task: asyncio.TimerHandle | None = None
|
2020-01-31 22:47:40 +00:00
|
|
|
self._execute_at_end_of_timer: bool = False
|
2020-02-26 19:27:37 +00:00
|
|
|
self._execute_lock = asyncio.Lock()
|
2021-03-17 17:34:19 +00:00
|
|
|
self._job: HassJob | None = None if function is None else HassJob(function)
|
2020-10-09 06:40:36 +00:00
|
|
|
|
|
|
|
@property
|
2021-03-17 17:34:19 +00:00
|
|
|
def function(self) -> Callable[..., Awaitable[Any]] | None:
|
2020-10-09 06:40:36 +00:00
|
|
|
"""Return the function being wrapped by the Debouncer."""
|
|
|
|
return self._function
|
|
|
|
|
|
|
|
@function.setter
|
|
|
|
def function(self, function: Callable[..., Awaitable[Any]]) -> None:
|
|
|
|
"""Update the function being wrapped by the Debouncer."""
|
|
|
|
self._function = function
|
|
|
|
if self._job is None or function != self._job.target:
|
|
|
|
self._job = HassJob(function)
|
2020-01-31 22:47:40 +00:00
|
|
|
|
|
|
|
async def async_call(self) -> None:
|
|
|
|
"""Call the function."""
|
2020-11-27 16:48:43 +00:00
|
|
|
assert self._job is not None
|
2020-01-31 22:47:40 +00:00
|
|
|
|
|
|
|
if self._timer_task:
|
|
|
|
if not self._execute_at_end_of_timer:
|
|
|
|
self._execute_at_end_of_timer = True
|
|
|
|
|
|
|
|
return
|
|
|
|
|
2020-02-26 19:27:37 +00:00
|
|
|
# Locked means a call is in progress. Any call is good, so abort.
|
|
|
|
if self._execute_lock.locked():
|
|
|
|
return
|
|
|
|
|
|
|
|
if not self.immediate:
|
2020-01-31 22:47:40 +00:00
|
|
|
self._execute_at_end_of_timer = True
|
2020-02-26 19:27:37 +00:00
|
|
|
self._schedule_timer()
|
|
|
|
return
|
2020-01-31 22:47:40 +00:00
|
|
|
|
2020-02-26 19:27:37 +00:00
|
|
|
async with self._execute_lock:
|
|
|
|
# Abort if timer got set while we're waiting for the lock.
|
|
|
|
if self._timer_task:
|
|
|
|
return
|
|
|
|
|
2020-11-27 16:48:43 +00:00
|
|
|
task = self.hass.async_run_hass_job(self._job)
|
|
|
|
if task:
|
|
|
|
await task
|
2020-02-26 19:27:37 +00:00
|
|
|
|
|
|
|
self._schedule_timer()
|
2020-01-31 22:47:40 +00:00
|
|
|
|
|
|
|
async def _handle_timer_finish(self) -> None:
|
|
|
|
"""Handle a finished timer."""
|
2020-11-27 16:48:43 +00:00
|
|
|
assert self._job is not None
|
2020-01-31 22:47:40 +00:00
|
|
|
|
|
|
|
self._timer_task = None
|
|
|
|
|
|
|
|
if not self._execute_at_end_of_timer:
|
|
|
|
return
|
|
|
|
|
|
|
|
self._execute_at_end_of_timer = False
|
|
|
|
|
2020-02-26 19:27:37 +00:00
|
|
|
# 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:
|
2020-11-27 16:48:43 +00:00
|
|
|
task = self.hass.async_run_hass_job(self._job)
|
|
|
|
if task:
|
|
|
|
await task
|
2020-02-26 19:27:37 +00:00
|
|
|
except Exception: # pylint: disable=broad-except
|
|
|
|
self.logger.exception("Unexpected exception from %s", self.function)
|
|
|
|
|
|
|
|
self._schedule_timer()
|
2020-01-31 22:47:40 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_cancel(self) -> None:
|
|
|
|
"""Cancel any scheduled call."""
|
|
|
|
if self._timer_task:
|
|
|
|
self._timer_task.cancel()
|
|
|
|
self._timer_task = None
|
|
|
|
|
|
|
|
self._execute_at_end_of_timer = False
|
2020-02-26 19:27:37 +00:00
|
|
|
|
|
|
|
@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()),
|
|
|
|
)
|