Browse Source

Merge branch 'master' into bulb_part1

J. Nick Koston 1 year ago
parent
commit
97e8eb423c
2 changed files with 85 additions and 14 deletions
  1. 2 2
      setup.py
  2. 83 12
      switchbot/devices/device.py

+ 2 - 2
setup.py

@@ -3,8 +3,8 @@ from setuptools import setup
 setup(
     name="PySwitchbot",
     packages=["switchbot", "switchbot.devices", "switchbot.adv_parsers"],
-    install_requires=["async_timeout>=4.0.1", "bleak", "bleak-retry-connector>=1.7.2"],
-    version="0.18.10",
+    install_requires=["async_timeout>=4.0.1", "bleak", "bleak-retry-connector>=1.11.0"],
+    version="0.18.14",
     description="A library to communicate with Switchbot",
     author="Daniel Hjelseth Hoyer",
     url="https://github.com/Danielhiversen/pySwitchbot/",

+ 83 - 12
switchbot/devices/device.py

@@ -10,6 +10,7 @@ from uuid import UUID
 import async_timeout
 
 from bleak import BleakError
+from bleak.exc import BleakDBusError
 from bleak.backends.device import BLEDevice
 from bleak.backends.service import BleakGATTCharacteristic, BleakGATTServiceCollection
 from bleak_retry_connector import (
@@ -41,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."""
 
@@ -52,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."""
 
@@ -104,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:
@@ -115,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(
@@ -164,15 +197,23 @@ class SwitchbotDevice:
                 self.name,
                 self._disconnected,
                 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:
@@ -198,30 +239,60 @@ class SwitchbotDevice:
     def _disconnect(self):
         """Disconnect from device."""
         self._disconnect_timer = None
-        asyncio.create_task(self._execute_disconnect())
+        asyncio.create_task(self._execute_timed_disconnect())
 
-    async def _execute_disconnect(self):
-        """Execute disconnection."""
+    async def _execute_timed_disconnect(self):
+        """Execute timed disconnection."""
         _LOGGER.debug(
             "%s: Disconnecting after timeout of %s",
             self.name,
             DISCONNECT_DELAY,
         )
+        await self._execute_disconnect()
+
+    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."""
         await self._ensure_connected()
+        try:
+            return await self._execute_command_locked(key, command)
+        except BleakDBusError as ex:
+            # Disconnect so we can reset state and try again
+            await asyncio.sleep(0.25)
+            _LOGGER.debug(
+                "%s: RSSI: %s; Backing off %ss; Disconnecting due to error: %s",
+                self.name,
+                self.rssi,
+                0.25,
+                ex,
+            )
+            await self._execute_disconnect()
+            raise
+        except BleakError as ex:
+            # Disconnect so we can reset state and try again
+            _LOGGER.debug(
+                "%s: RSSI: %s; Disconnecting due to error: %s", self.name, self.rssi, ex
+            )
+            await self._execute_disconnect()
+            raise
+
+    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