Browse Source

Avoid caching services when there are missing Characteristics (#84)

J. Nick Koston 1 year ago
parent
commit
017a08ab5b
1 changed files with 50 additions and 9 deletions
  1. 50 9
      switchbot/devices/device.py

+ 50 - 9
switchbot/devices/device.py

@@ -42,6 +42,10 @@ BLEAK_EXCEPTIONS = (AttributeError, BleakError, asyncio.exceptions.TimeoutError)
 DISCONNECT_DELAY = 49
 
 
+class CharacteristicMissingError(Exception):
+    """Raised when a characteristic is missing."""
+
+
 def _sb_uuid(comms_type: str = "service") -> UUID | str:
     """Return Switchbot UUID."""
 
@@ -53,6 +57,10 @@ def _sb_uuid(comms_type: str = "service") -> UUID | str:
     return "Incorrect type, choose between: tx, rx or service"
 
 
+READ_CHAR_UUID = _sb_uuid(comms_type="rx")
+WRITE_CHAR_UUID = _sb_uuid(comms_type="tx")
+
+
 class SwitchbotDevice:
     """Base Representation of a Switchbot Device."""
 
@@ -105,6 +113,12 @@ class SwitchbotDevice:
             )
 
         max_attempts = retry + 1
+        if self._operation_lock.locked():
+            _LOGGER.debug(
+                "%s: Operation already in progress, waiting for it to complete; RSSI: %s",
+                self.name,
+                self.rssi,
+            )
         async with self._operation_lock:
             for attempt in range(max_attempts):
                 try:
@@ -116,6 +130,24 @@ class SwitchbotDevice:
                         exc_info=True,
                     )
                     return b"\x00"
+                except CharacteristicMissingError as ex:
+                    if attempt == retry:
+                        _LOGGER.error(
+                            "%s: characteristic missing: %s; Stopping trying; RSSI: %s",
+                            self.name,
+                            ex,
+                            self.rssi,
+                            exc_info=True,
+                        )
+                        return b"\x00"
+
+                    _LOGGER.debug(
+                        "%s: characteristic missing: %s; RSSI: %s",
+                        self.name,
+                        ex,
+                        self.rssi,
+                        exc_info=True,
+                    )
                 except BLEAK_EXCEPTIONS:
                     if attempt == retry:
                         _LOGGER.error(
@@ -167,14 +199,21 @@ class SwitchbotDevice:
                 cached_services=self._cached_services,
                 ble_device_callback=lambda: self._device,
             )
-            self._cached_services = client.services
             _LOGGER.debug("%s: Connected; RSSI: %s", self.name, self.rssi)
-            services = client.services
-            self._read_char = services.get_characteristic(_sb_uuid(comms_type="rx"))
-            self._write_char = services.get_characteristic(_sb_uuid(comms_type="tx"))
+            resolved = self._resolve_characteristics(client.services)
+            if not resolved:
+                # Try to handle services failing to load
+                resolved = self._resolve_characteristics(await client.get_services())
+            self._cached_services = client.services if resolved else None
             self._client = client
             self._reset_disconnect_timer()
 
+    def _resolve_characteristics(self, services: BleakGATTServiceCollection) -> bool:
+        """Resolve characteristics."""
+        self._read_char = services.get_characteristic(READ_CHAR_UUID)
+        self._write_char = services.get_characteristic(WRITE_CHAR_UUID)
+        return bool(self._read_char and self._write_char)
+
     def _reset_disconnect_timer(self):
         """Reset disconnect timer."""
         if self._disconnect_timer:
@@ -214,13 +253,13 @@ class SwitchbotDevice:
     async def _execute_disconnect(self):
         """Execute disconnection."""
         async with self._connect_lock:
-            if not self._client or not self._client.is_connected:
-                return
+            client = self._client
             self._expected_disconnect = True
-            await self._client.disconnect()
             self._client = None
             self._read_char = None
             self._write_char = None
+            if client and client.is_connected:
+                await client.disconnect()
 
     async def _send_command_locked(self, key: str, command: bytes) -> bytes:
         """Send command to device and read response."""
@@ -250,8 +289,10 @@ class SwitchbotDevice:
     async def _execute_command_locked(self, key: str, command: bytes) -> bytes:
         """Execute command and read response."""
         assert self._client is not None
-        assert self._read_char is not None
-        assert self._write_char is not None
+        if not self._read_char:
+            raise CharacteristicMissingError(READ_CHAR_UUID)
+        if not self._write_char:
+            raise CharacteristicMissingError(WRITE_CHAR_UUID)
         future: asyncio.Future[bytearray] = asyncio.Future()
         client = self._client