"""The bluetooth utilities.""" from __future__ import annotations import asyncio import functools from contextlib import suppress from functools import cache from pathlib import Path from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar from bluetooth_auto_recovery import recover_adapter if TYPE_CHECKING: from collections.abc import Callable, Coroutine _P = ParamSpec("_P") _R = TypeVar("_R") async def async_reset_adapter( adapter: str | None, mac_address: str, gone_silent: bool ) -> bool | None: """Reset the adapter.""" if adapter and adapter.startswith("hci"): adapter_id = int(adapter[3:]) return await recover_adapter(adapter_id, mac_address, gone_silent) return False @cache def is_docker_env() -> bool: """Return True if we run in a docker env.""" return Path("/.dockerenv").exists() def coalesce_concurrent_future( attr: str, ) -> Callable[ [Callable[_P, Coroutine[Any, Any, _R]]], Callable[_P, Coroutine[Any, Any, _R]], ]: """ Coalesce concurrent async method calls onto a single shared future. Mirrors the home-assistant ``loader.py`` shared-future pattern. The first caller runs the wrapped coroutine and the result (or exception) is published on a future stored at ``self.``; concurrent callers wait on the same future and observe the same outcome. ``asyncio.wait`` is used on the waiter side so a cancelled waiter does not transitively cancel the shared future and strand the leader or its siblings. Cancellation contract: if the leader is cancelled the ``CancelledError`` is forwarded as-is to every waiter via ``set_exception`` (matching ``loader.py``); callers that need to be insulated from leader cancellation should wrap their own call in ``asyncio.shield``. Pre-condition: ``self.`` must already exist on the instance and be initialised to ``None`` before the first call. The decorator reads it via ``getattr`` (no default) and resets it to ``None`` in ``finally`` once the leader completes. Only usable on instance methods, ``self`` is taken from ``args[0]``. """ def decorator( func: Callable[_P, Coroutine[Any, Any, _R]], ) -> Callable[_P, Coroutine[Any, Any, _R]]: @functools.wraps(func) async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: self = args[0] future: asyncio.Future[_R] | None = getattr(self, attr) if future is not None: await asyncio.wait((future,)) return future.result() future = asyncio.get_running_loop().create_future() setattr(self, attr, future) try: result = await func(*args, **kwargs) except BaseException as ex: future.set_exception(ex) # Mark the exception as retrieved so asyncio does not warn # when no concurrent waiters consume it. with suppress(BaseException): future.result() raise else: future.set_result(result) return result finally: setattr(self, attr, None) return wrapper return decorator