diff --git a/custom_components/tuya_local_ble/tuya_ble/const.py b/custom_components/tuya_local_ble/tuya_ble/const.py index b1514a9..c73f268 100644 --- a/custom_components/tuya_local_ble/tuya_ble/const.py +++ b/custom_components/tuya_local_ble/tuya_ble/const.py @@ -9,6 +9,12 @@ DEFAULT_ATTEMPTS = 0xFFFF CHARACTERISTIC_NOTIFY = "00002b10-0000-1000-8000-00805f9b34fb" CHARACTERISTIC_WRITE = "00002b11-0000-1000-8000-00805f9b34fb" +# Newer TuyaOS firmware (e.g. jtmspro hc7n0urm fw 6.0) drops the legacy +# 0xA201 service and exposes the data channel via these characteristics +# of the 0xFD50 service instead. +CHARACTERISTIC_NOTIFY_FD50 = "00000002-0000-1001-8001-00805f9b07d0" +CHARACTERISTIC_WRITE_FD50 = "00000001-0000-1001-8001-00805f9b07d0" + SERVICE_UUID = "0000a201-0000-1000-8000-00805f9b34fb" MANUFACTURER_DATA_ID = 0x07D0 diff --git a/custom_components/tuya_local_ble/tuya_ble/tuya_ble.py b/custom_components/tuya_local_ble/tuya_ble/tuya_ble.py index 274cbf6..73049eb 100644 --- a/custom_components/tuya_local_ble/tuya_ble/tuya_ble.py +++ b/custom_components/tuya_local_ble/tuya_ble/tuya_ble.py @@ -24,7 +24,9 @@ from Crypto.Cipher import AES from .const import ( CHARACTERISTIC_NOTIFY, + CHARACTERISTIC_NOTIFY_FD50, CHARACTERISTIC_WRITE, + CHARACTERISTIC_WRITE_FD50, GATT_MTU, MANUFACTURER_DATA_ID, RESPONSE_WAIT_TIMEOUT, @@ -231,6 +233,9 @@ class TuyaBLEDevice: self._current_seq_num = 1 self._seq_num_lock = asyncio.Lock() + self._characteristic_notify = CHARACTERISTIC_NOTIFY + self._characteristic_write = CHARACTERISTIC_WRITE + self._is_bound = False self._flags = 0 self._protocol_version = 2 @@ -540,11 +545,25 @@ class TuyaBLEDevice: self._expected_disconnect = True self._client = None if client and client.is_connected: - await client.stop_notify(CHARACTERISTIC_NOTIFY) + await client.stop_notify(self._characteristic_notify) await client.disconnect() async with self._seq_num_lock: self._current_seq_num = 1 + def _select_characteristics(self, client: BleakClientWithServiceCache) -> None: + """Select the GATT channel actually exposed by the device.""" + if client.services.get_characteristic(CHARACTERISTIC_NOTIFY): + self._characteristic_notify = CHARACTERISTIC_NOTIFY + self._characteristic_write = CHARACTERISTIC_WRITE + elif client.services.get_characteristic(CHARACTERISTIC_NOTIFY_FD50): + _LOGGER.debug( + "%s: legacy characteristics not present," + " using FD50 GATT channel", + self.address, + ) + self._characteristic_notify = CHARACTERISTIC_NOTIFY_FD50 + self._characteristic_write = CHARACTERISTIC_WRITE_FD50 + async def _ensure_connected(self) -> None: """Ensure connection to device is established.""" global global_connect_lock @@ -609,9 +628,10 @@ class TuyaBLEDevice: _LOGGER.debug("%s: Connected; RSSI: %s", self.address, self.rssi) self._client = client + self._select_characteristics(client) try: await self._client.start_notify( - CHARACTERISTIC_NOTIFY, + self._characteristic_notify, self._notification_handler, bluez={"use_start_notify": True}, ) @@ -968,7 +988,7 @@ class TuyaBLEDevice: try: # _LOGGER.debug("%s: Sending packet: %s", self.address, packet.hex()) await self._client.write_gatt_char( - CHARACTERISTIC_WRITE, + self._characteristic_write, packet, False, )