88from typing import TYPE_CHECKING , Any , Literal , TypeVar , cast
99
1010if TYPE_CHECKING :
11- from collections .abc import Mapping
11+ from collections .abc import Awaitable , Callable , Mapping
1212
1313import aiohttp
1414import backoff
@@ -259,6 +259,40 @@ def _apply_bus_specific_api_config(self) -> None:
259259 self ._api_data ["heating_circuit2" ] = {}
260260 self ._api_data ["staticValues_circuit2" ] = {}
261261
262+ async def _run_once_locked (
263+ self ,
264+ key : str ,
265+ locks : dict [str , asyncio .Lock ],
266+ is_done : Callable [[], bool ],
267+ body : Callable [[], Awaitable [None ]],
268+ ) -> None :
269+ """Run an idempotent async operation at most once per key.
270+
271+ Implements double-checked locking: a fast path when the work is already
272+ done, a per-key lock to serialize concurrent first-time callers, and a
273+ re-check after acquiring the lock so only one caller runs ``body``.
274+
275+ Args:
276+ key (str): Identifier for the one-time operation.
277+ locks (dict[str, asyncio.Lock]): Registry of per-key locks, created
278+ on demand.
279+ is_done (Callable[[], bool]): Predicate returning True when the work
280+ is already complete.
281+ body (Callable[[], Awaitable[None]]): Coroutine factory executed once
282+ while holding the lock.
283+
284+ """
285+ if is_done ():
286+ return
287+
288+ if key not in locks :
289+ locks [key ] = asyncio .Lock ()
290+
291+ async with locks [key ]:
292+ if is_done ():
293+ return
294+ await body ()
295+
262296 async def _ensure_section_validated (
263297 self , section : SectionLiteral , include : list [str ] | None = None
264298 ) -> None :
@@ -279,19 +313,7 @@ async def _ensure_section_validated(
279313 if not self ._api_validator :
280314 raise BSBLANError (ErrorMsg .API_VALIDATOR_NOT_INITIALIZED )
281315
282- # Fast path: skip if already validated (no lock needed)
283- if self ._api_validator .is_section_validated (section ):
284- return
285-
286- # Get or create lock for this section
287- if section not in self ._section_locks :
288- self ._section_locks [section ] = asyncio .Lock ()
289-
290- async with self ._section_locks [section ]:
291- # Double-check after acquiring lock (another task may have validated)
292- if self ._api_validator .is_section_validated (section ):
293- return
294-
316+ async def _validate () -> None :
295317 logger .debug ("Lazy loading section: %s" , section )
296318 response_data = await self ._validate_api_section (section , include )
297319
@@ -300,6 +322,13 @@ async def _ensure_section_validated(
300322 ):
301323 self ._extract_temperature_unit_from_response (response_data )
302324
325+ await self ._run_once_locked (
326+ section ,
327+ self ._section_locks ,
328+ lambda : self ._api_validator .is_section_validated (section ),
329+ _validate ,
330+ )
331+
303332 def _should_extract_temperature_unit (
304333 self ,
305334 section : SectionLiteral ,
@@ -337,24 +366,13 @@ async def _ensure_hot_water_group_validated(
337366 If provided, only these parameters will be validated.
338367
339368 """
340- # Fast path: skip if already validated (no lock needed)
341- if group_name in self ._validated_hot_water_groups :
342- return
343-
344- if not self ._api_validator :
345- raise BSBLANError (ErrorMsg .API_VALIDATOR_NOT_INITIALIZED )
346369
347- if not self ._api_data :
348- raise BSBLANError (ErrorMsg .API_DATA_NOT_INITIALIZED )
370+ async def _validate () -> None :
371+ if not self ._api_validator :
372+ raise BSBLANError (ErrorMsg .API_VALIDATOR_NOT_INITIALIZED )
349373
350- # Get or create lock for this group
351- if group_name not in self ._hot_water_group_locks :
352- self ._hot_water_group_locks [group_name ] = asyncio .Lock ()
353-
354- async with self ._hot_water_group_locks [group_name ]:
355- # Double-check after acquiring lock (another task may have validated)
356- if group_name in self ._validated_hot_water_groups :
357- return
374+ if not self ._api_data :
375+ raise BSBLANError (ErrorMsg .API_DATA_NOT_INITIALIZED )
358376
359377 logger .debug ("Lazy loading hot water group: %s" , group_name )
360378
@@ -423,6 +441,13 @@ async def _ensure_hot_water_group_validated(
423441 len (params_to_remove ),
424442 )
425443
444+ await self ._run_once_locked (
445+ group_name ,
446+ self ._hot_water_group_locks ,
447+ lambda : group_name in self ._validated_hot_water_groups ,
448+ _validate ,
449+ )
450+
426451 async def _validate_api_section (
427452 self , section : SectionLiteral , include : list [str ] | None = None
428453 ) -> dict [str , Any ] | None :
0 commit comments