From 982b7ae1cc12d50a899329466fd4b760aaaec5ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Oct 2022 08:07:07 -0500 Subject: [PATCH] feat: improve handling of out of esp32 proxy connection slots (#56) --- src/bleak_retry_connector/__init__.py | 31 ++++++++++++++++------ tests/test_init.py | 37 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/bleak_retry_connector/__init__.py b/src/bleak_retry_connector/__init__.py index b920fff..b8a0a2b 100644 --- a/src/bleak_retry_connector/__init__.py +++ b/src/bleak_retry_connector/__init__.py @@ -40,6 +40,7 @@ # to run their cleanup callbacks or the # retry call will just fail in the same way. BLEAK_DBUS_BACKOFF_TIME = 0.25 +BLEAK_OUT_OF_SLOTS_BACKOFF_TIME = 1.5 BLEAK_BACKOFF_TIME = 0.1 @@ -91,6 +92,8 @@ DEVICE_MISSING_ERRORS = {"org.freedesktop.DBus.Error.UnknownObject"} +OUT_OF_SLOTS_ERRORS = {"available connection", "connection slot"} + # Currently the same as transient error ABORT_ERRORS = TRANSIENT_ERRORS @@ -104,6 +107,11 @@ "The device disappeared; " "Try restarting the scanner or moving the device closer" ) +OUT_OF_SLOTS_ADVICE = ( + "The proxy/adapter is out of connection slots; " + "Add additional proxies near this device" +) + class BleakNotFoundError(BleakError): """The device was not found.""" @@ -117,6 +125,10 @@ class BleakAbortedError(BleakError): """The connection was aborted.""" +class BleakOutOfConnectionSlotsError(BleakError): + """The proxy/adapter is out of connection slots.""" + + class BleakClientWithServiceCache(BleakClient): """A BleakClient that implements service caching.""" @@ -183,6 +195,8 @@ def address_to_bluez_path(address: str, adapter: str | None = None) -> str: def calculate_backoff_time(exc: Exception) -> float: """Calculate the backoff time based on the exception.""" + if isinstance(exc, BleakOutOfConnectionSlotsError): + return BLEAK_OUT_OF_SLOTS_BACKOFF_TIME if isinstance( exc, (BleakDBusError, EOFError, asyncio.TimeoutError, BrokenPipeError) ): @@ -431,14 +445,15 @@ def _raise_if_needed(name: str, description: str, exc: Exception) -> None: # Sure would be nice if bleak gave us typed exceptions if isinstance(exc, asyncio.TimeoutError) or "not found" in str(exc): raise BleakNotFoundError(msg) from exc - if isinstance(exc, BleakError) and any( - error in str(exc) for error in ABORT_ERRORS - ): - raise BleakAbortedError(f"{msg}: {ABORT_ADVICE}") from exc - if isinstance(exc, BleakError) and any( - error in str(exc) for error in DEVICE_MISSING_ERRORS - ): - raise BleakNotFoundError(f"{msg}: {DEVICE_MISSING_ADVICE}") from exc + if isinstance(exc, BleakError): + if any(error in str(exc) for error in ABORT_ERRORS): + raise BleakAbortedError(f"{msg}: {ABORT_ADVICE}") from exc + if any(error in str(exc) for error in DEVICE_MISSING_ERRORS): + raise BleakNotFoundError(f"{msg}: {DEVICE_MISSING_ADVICE}") from exc + if any(error in str(exc) for error in OUT_OF_SLOTS_ERRORS): + raise BleakOutOfConnectionSlotsError( + f"{msg}: {OUT_OF_SLOTS_ADVICE}" + ) from exc raise BleakConnectionError(msg) from exc create_client = True diff --git a/tests/test_init.py b/tests/test_init.py index 63adbe4..c45a183 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -13,11 +13,13 @@ from bleak_retry_connector import ( BLEAK_BACKOFF_TIME, BLEAK_DBUS_BACKOFF_TIME, + BLEAK_OUT_OF_SLOTS_BACKOFF_TIME, MAX_TRANSIENT_ERRORS, BleakAbortedError, BleakClientWithServiceCache, BleakConnectionError, BleakNotFoundError, + BleakOutOfConnectionSlotsError, ble_device_has_changed, calculate_backoff_time, establish_connection, @@ -434,6 +436,37 @@ async def disconnect(self, *args, **kwargs): ) +@pytest.mark.asyncio +async def test_establish_connection_out_of_slots_advice(): + class FakeBleakClient(BleakClient): + def __init__(self, *args, **kwargs): + pass + + async def connect(self, *args, **kwargs): + raise BleakError("out of connection slots") + + async def disconnect(self, *args, **kwargs): + pass + + try: + await establish_connection( + FakeBleakClient, + BLEDevice("aa:bb:cc:dd:ee:ff", "name", {"path": "/dev/1"}), + "test", + ) + except BleakError as e: + exc = e + + assert isinstance(exc, BleakOutOfConnectionSlotsError) + assert str(exc) == ( + "test - /dev/1: " + "Failed to connect: " + "out of connection slots: " + "The proxy/adapter is out of connection slots; " + "Add additional proxies near this device" + ) + + @pytest.mark.asyncio async def test_device_disappeared_error(): class FakeBleakClient(BleakClient): @@ -1443,6 +1476,10 @@ def test_calculate_backoff_time(): calculate_backoff_time(BleakDBusError(MagicMock(), MagicMock())) == BLEAK_DBUS_BACKOFF_TIME ) + assert ( + calculate_backoff_time(BleakOutOfConnectionSlotsError()) + == BLEAK_OUT_OF_SLOTS_BACKOFF_TIME + ) @pytest.mark.asyncio