From 6ea15cc5e5cab24df67a4925b08bd14264fb379e Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky Date: Sun, 15 Feb 2026 15:24:22 +0200 Subject: [PATCH 1/2] Add API rate limiter with exponential backoff retry Add a semaphore-based rate limiter (max 3 concurrent requests) and a helper method for executing API calls with retry logic and exponential backoff (3 retries, starting at 1s delay). --- custom_components/iec/coordinator.py | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/custom_components/iec/coordinator.py b/custom_components/iec/coordinator.py index 9136c58..4cd8d54 100644 --- a/custom_components/iec/coordinator.py +++ b/custom_components/iec/coordinator.py @@ -118,6 +118,7 @@ def __init__( session=aiohttp_client.async_get_clientsession(hass, family=socket.AF_INET), ) self._first_load: bool = True + self._api_rate_limiter = asyncio.Semaphore(3) @callback def _dummy_listener() -> None: @@ -133,6 +134,48 @@ async def async_unload(self): """Unload the coordinator, cancel any pending tasks.""" _LOGGER.info("Coordinator unloaded successfully.") + async def _execute_with_rate_limit( + self, + coro, + max_retries: int = 3, + base_delay: float = 1.0, + ): + """Execute an API call with rate limiting and retry logic. + + Args: + coro: The coroutine to execute + max_retries: Maximum number of retries + base_delay: Base delay in seconds for exponential backoff + + Returns: + The result of the coroutine + + Raises: + The last exception if all retries fail + """ + last_exception = None + for attempt in range(max_retries): + async with self._api_rate_limiter: + try: + return await coro + except IECError as e: + last_exception = e + if attempt < max_retries - 1: + delay = base_delay * (2**attempt) + _LOGGER.warning( + "API call failed (attempt %d/%d), retrying in %.1fs: %s", + attempt + 1, + max_retries, + delay, + e, + ) + await asyncio.sleep(delay) + else: + _LOGGER.error( + "API call failed after %d attempts: %s", max_retries, e + ) + raise last_exception + async def _get_devices_by_contract_id(self, contract_id) -> list[Device]: devices = self._devices_by_contract_id.get(contract_id) if not devices: From 50655aabfa2b66c242607c3d6d2e74be85f26511 Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky Date: Sun, 15 Feb 2026 15:43:05 +0200 Subject: [PATCH 2/2] Fix ruff lint errors --- custom_components/iec/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/iec/coordinator.py b/custom_components/iec/coordinator.py index 4cd8d54..562a1cf 100644 --- a/custom_components/iec/coordinator.py +++ b/custom_components/iec/coordinator.py @@ -152,6 +152,7 @@ async def _execute_with_rate_limit( Raises: The last exception if all retries fail + """ last_exception = None for attempt in range(max_retries):