Repository: hbldh/bleak (language: python)

Issue:
I'm trying to connect to a BLE device via bleak - it works fine when I use client.start_notify on Windows 11 and andriod , but script fails , My logcat error: bleak.exc.BleakError: Descriptor None was not found!
@dlech @hbldh
    



    def scan1(self):
        
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self.begin_scan1())

    async def begin_scan1(self):

        def callback2(sender, data):
            print(sender, data, 'llllll')
            self.val1 = data
            
        a = []
        scanned_devices = await bleak.BleakScanner.discover(1)

        if len(scanned_devices) == 0:
            print("NO DEVICES")
        scanned_devices.sort(key=lambda device: -device.rssi)
        for device in scanned_devices:
            device1 = f"{device.name}"
            print(device1)
            if device1 == "Bluno":
                # try:

                async with bleak.BleakClient(device) as client:
                    print('rr')
                    paired = client.pair(protection_level=2)
                    print(f"Paired: {paired}")
                    COLOR_CHARACTERISTIC = "0000dfb1-0000-1000-8000-00805f9b34fb"
                     

                    uu = '1'
                    write_value = bytearray([int(uu)])
                    await client.write_gatt_char(COLOR_CHARACTERISTIC, write_value)
                    await asyncio.sleep(4.0)
                    print('www')
                    await client.start_notify(COLOR_CHARACTERISTIC, callback2)
                    print('wwee')



                    await asyncio.sleep(10.0)
                    print('rrrq')
                    await client.stop_notify(COLOR_CHARACTERISTIC)
               


Discussion / hints:
bleak.exc.BleakError: Descriptor None was not found!
Can out share the output of https://github.com/hbldh/bleak/blob/develop/examples/service_explorer.py when connected to this device?
@dlech @hbldh 


INFO:bleak.backends.winrt.client:Services resolved for BleakClientWinRT (C4:BE:84:20:1B:59)
INFO:__main__:Connected: True
INFO:__main__:[Service] 00001800-0000-1000-8000-00805f9b34fb (Handle: 1): Generic Access Profile
INFO:__main__:    [Characteristic] 00002a00-0000-1000-8000-00805f9b34fb (Handle: 2):  (read), Value: b'Bluno'
INFO:__main__:    [Characteristic] 00002a01-0000-1000-8000-00805f9b34fb (Handle: 4):  (read), Value: b'\x00\x00'
INFO:__main__:    [Characteristic] 00002a02-0000-1000-8000-00805f9b34fb (Handle: 6):  (read), Value: b'\x00'
INFO:__main__:    [Characteristic] 00002a03-0000-1000-8000-00805f9b34fb (Handle: 8):  (write), Value: None
INFO:__main__:    [Characteristic] 00002a04-0000-1000-8000-00805f9b34fb (Handle: 10):  (read), Value: b'P\x00\xa0\x00\x00\x00\xe8\x03'
INFO:__main__:[Service] 00001801-0000-1000-8000-00805f9b34fb (Handle: 12): Generic Attribute Profile
INFO:__main__:    [Characteristic] 00002a05-0000-1000-8000-00805f9b34fb (Handle: 13):  (indicate), Value: None
INFO:__main__:        [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 15): Client Characteristic Configuration) | Value: b'\x02\x00'
INFO:__main__:[Service] 0000180a-0000-1000-8000-00805f9b34fb (Handle: 16): Device Information
INFO:__main__:    [Characteristic] 00002a23-0000-1000-8000-00805f9b34fb (Handle: 17):  (read), Value: b'Y\x1b \x00\x00\x84\xbe\xc4'
INFO:__main__:    [Characteristic] 00002a24-0000-1000-8000-00805f9b34fb (Handle: 19):  (read), Value: b'DF Bluno'
INFO:__main__:    [Characteristic] 00002a25-0000-1000-8000-00805f9b34fb (Handle: 21):  (read), Value: b'0123456789'
INFO:__main__:    [Characteristic] 00002a26-0000-1000-8000-00805f9b34fb (Handle: 23):  (read), Value: b'FW V1.97'
INFO:__main__:    [Characteristic] 00002a27-0000-1000-8000-00805f9b34fb (Handle: 25):  (read), Value: b'HW V1.7'
INFO:__main__:    [Characteristic] 00002a28-0000-1000-8000-00805f9b34fb (Handle: 27):  (read), Value: b'SW V1.97'
INFO:__main__:    [Characteristic] 00002a29-0000-1000-8000-00805f9b34fb (Handle: 29):  (read), Value: b'DFRobot'
INFO:__main__:    [Characteristic] 00002a2a-0000-1000-8000-00805f9b34fb (Handle: 31):  (read), Value: b'\xfe\x00experimental'
INFO:__main__:    [Characteristic] 00002a50-0000-1000-8000-00805f9b34fb (Handle: 33):  (read), Value: b'\x01\r\x00\x00\x00\x10\x01'
INFO:__main__:[Service] 0000dfb0-0000-1000-8000-00805f9b34fb (Handle: 35): Vendor specific
INFO:__main__:    [Characteristic] 0000dfb1-0000-1000-8000-00805f9b34fb (Handle: 36): Serial (read,write-without-response,write,notify), Value: b'\x01'
INFO:__main__:        [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 38): Characteristic User Description) | Value: b'Serial'
INFO:__main__:    [Characteristic] 0000dfb2-0000-1000-8000-00805f9b34fb (Handle: 39): AT Ctrl (read,write-without-response,write,notify), Value: b'\x02'
INFO:__main__:        [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 41): Characteristic User Description) | Value: b'AT Ctrl'
Process finished with exit code 0
> INFO:**main**: [Characteristic] 0000dfb1-0000-1000-8000-00805f9b34fb (Handle: 36): Serial (read,write-without-response,write,notify), Value: b'\x01'
> INFO:**main**: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 38): Characteristic User Description) | Value: b'Serial'

This is the "COLOR" characteristic. It does not have a "Client Characteristic Configuration" descriptor, so that explains the error seen on Android. I'm not sure how it works on Windows. You could capture Bluetooth packets on Windows to get a better idea of what is going on under the hood on Windows.

Perhaps we just need to surround https://github.com/hbldh/bleak/blob/46c828b2f856b717394414957258fb77f6fb678a/bleak/backends/p4android/client.py#L497-L500 with `if characteristic.notification_descriptor:`.
@dlech @hbldh 
 The "COLOR" characteristic is just a variable for the UUID . As seen from the get_services list only the '0000dfb1-0000-1000-8000-00805f9b34fb' UUID has the capabilities to read and write data from device to hardware. We did try surrounding 
 if characteristic.notification_descriptor:
          await self.write_gatt_descriptor( 
               characteristic.notification_descriptor, 
               defs.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, 
           ) 
still no solution

> > INFO:**main**: [Characteristic] 0000dfb1-0000-1000-8000-00805f9b34fb (Handle: 36): Serial (read,write-without-response,write,notify), Value: b'\x01'
> > INFO:**main**: [Descriptor] 00002901-0000-1000-8000-00805f9b34fb (Handle: 38): Characteristic User Description) | Value: b'Serial'
> 
> This is the "COLOR" characteristic. It does not have a "Client Characteristic Configuration" descriptor, so that explains the error seen on Android. I'm not sure how it works on Windows. You could capture Bluetooth packets on Windows to get a better idea of what is going on under the hood on Windows.
> 
> Perhaps we just need to surround
> 
> https://github.com/hbldh/bleak/blob/46c828b2f856b717394414957258fb77f6fb678a/bleak/backends/p4android/client.py#L497-L500
> 
> with `if characteristic.notification_descriptor:`.

@dlech @hbldh 
 The "COLOR" characteristic is just a variable for the UUID . As seen from the get_services list only the '0000dfb1-0000-1000-8000-00805f9b34fb' UUID has the capabilities to read and write data from device to hardware. We did try surrounding 
 if characteristic.notification_descriptor:
          await self.write_gatt_descriptor( 
               characteristic.notification_descriptor, 
               defs.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, 
           ) 
able to write data with this UUID but for reading there is no function neither read_gatt_char works for start_notify!!!
@dlech @hbldh 
Any update on the issue?
I ran into the same thing, my fix was to pin bleak to 0.20.2 which resolves the "error" and makes the call to start_notify() work as expected.

I haven't checked if the pinning causes other packets to be downgraded (since the problem is currently time sensitive) but i do as of now assume that this was caused by a change in bleak.
@apacodes it sounds like there is a regression between 0.20.2 and 0.21.0? If so, can you please start a new issue with more detail about the problem?
Yes.

I will investigate an do so in the coming days.
This issue may because of files in: bleak/backends/p4android
defs.py last line #89:

CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = "2902"

characteristic.py #90

        if descriptor.uuid == defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID:
            self.__notification_descriptor = descriptor

Maybe we need to use back the full string:

CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = "00002902-0000-1000-8000-00805f9b34fb"
Hello,

I'm currently having the same error (Descriptor None was not found).

I've solved the issue by monkey patching the CLIENT_CHARACTERISTIC_CONFIGURATION_UUID constant at startup in my app using :
```
from bleak.backends.p4android import defs
defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = "00002902-0000-1000-8000-00805f9b34fb"
```

I think `bleak/backends/p4android/characteristic.py` should check for both the full and short uuid constant to avoid regression.

(Bleak version 0.21.1 with Python-for-Android, Kivy and Buildozer, running on Android 13.1)
Thanks for confirming. I would take a pull request for this. I think the short version is wrong and we don't need to have both.
Confirming this as well. I tried downgrading to 0.20.2 but it did not work either.

```
04-06 14:56:18.361 13037 13062 I python  :  Traceback (most recent call last):
04-06 14:56:18.361 13037 13062 I python  :    File "/home/narodnik/src/pump/ecgapp/.buildozer/android/app/main.py", line 101, in <module>
04-06 14:56:18.362 13037 13062 I python  :    File "/home/narodnik/src/pump/ecgapp/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/other_builds/python3/arm64-v8a__ndk_target_21/python3/Lib/asyncio/base_events.py", line 653, in run_until_complete
04-06 14:56:18.363 13037 13062 I python  :    File "/home/narodnik/src/pump/ecgapp/.buildozer/android/app/main.py", line 67, in monitor_hr
04-06 14:56:18.364 13037 13062 I python  :    File "/home/narodnik/src/pump/ecgapp/.buildozer/android/app/main.py", line 76, in run_sensor
04-06 14:56:18.364 13037 13062 I python  :    File "/home/narodnik/src/pump/ecgapp/.buildozer/android/app/polarh10.py", line 295, in start_hr_stream
04-06 14:56:18.365 13037 13062 I python  :    File "/home/narodnik/src/pump/ecgapp/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/ecgapp/arm64-v8a/bleak/__init__.py", line 834, in start_notify
04-06 14:56:18.366 13037 13062 I python  :    File "/home/narodnik/src/pump/ecgapp/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/ecgapp/arm64-v8a/bleak/backends/p4android/client.py", line 450, in start_notify
04-06 14:56:18.366 13037 13062 I python  :    File "/home/narodnik/src/pump/ecgapp/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/ecgapp/arm64-v8a/bleak/backends/p4android/client.py", line 418, in write_gatt_descriptor
04-06 14:56:18.367 13037 13062 I python  :  bleak.exc.BleakError: Descriptor None was not found!

```

The monkey patch above worked though.

I also had to copy the .java files as documented in another issue.

Fix patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 77f18e30..c2d9b43c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -10,6 +10,22 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
 `Unreleased`_
 =============
 
+`0.22.2`_ (2024-06-01)
+======================
+
+Changed
+-------
+* Retrieve the BLE address required by ``BleakClientWinRT`` from scan response if advertising is None (WinRT).
+* Changed type hint for ``adv`` attribute of ``bleak.backends.winrt.scanner._RawAdvData``.
+* ``BleakGATTCharacteristic.max_write_without_response_size`` is now dynamic.
+
+Fixed
+-----
+* Fixed ``discovered_devices_and_advertisement_data`` returning devices that should
+  be filtered out by service UUIDs. Fixes #1576.
+* Fixed a ``Descriptor None was not found!`` exception occurring in ``start_notify()`` on Android. Fixes #823.
+* Fixed exception raised when starting ``BleakScanner`` while running in a Windows GUI app.
+
 `0.22.1`_ (2024-05-07)
 ======================
 
@@ -44,7 +60,7 @@ Fixed
 * Fixed scanning silently failing on Windows when Bluetooth is off. Fixes #1535.
 * Fixed using wrong value for ``tx_power`` in Android backend. Fixes #1532.
 * Fixed 4-character UUIDs not working on ``BleakClient.*_gatt_char`` methods. Fixes #1498.
-* Fixed race condition with getting max PDU size on Windows. Fixes #1497.
+* Fixed race condition with getting max PDU size on Windows. Fixes #1497. [REVERTED in v0.22.2]
 * Fixed filtering advertisement data by service UUID when multiple apps are scanning. Fixes #1534.
 
 `0.21.1`_ (2023-09-08)
@@ -1022,7 +1038,8 @@ Fixed
 * Bleak created.
 
 
-.. _Unreleased: https://github.com/hbldh/bleak/compare/v0.22.1...develop
+.. _Unreleased: https://github.com/hbldh/bleak/compare/v0.22.2...develop
+.. _0.22.2: https://github.com/hbldh/bleak/compare/v0.22.1...v0.22.2
 .. _0.22.1: https://github.com/hbldh/bleak/compare/v0.22.0...v0.22.1
 .. _0.22.0: https://github.com/hbldh/bleak/compare/v0.21.1...v0.22.0
 .. _0.21.1: https://github.com/hbldh/bleak/compare/v0.21.0...v0.21.1
diff --git a/bleak/backends/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py
index 3b41147b..ab707827 100644
--- a/bleak/backends/bluezdbus/characteristic.py
+++ b/bleak/backends/bluezdbus/characteristic.py
@@ -1,4 +1,4 @@
-from typing import List, Union
+from typing import Callable, List, Union
 from uuid import UUID
 
 from ..characteristic import BleakGATTCharacteristic
@@ -36,7 +36,7 @@ def __init__(
         object_path: str,
         service_uuid: str,
         service_handle: int,
-        max_write_without_response_size: int,
+        max_write_without_response_size: Callable[[], int],
     ):
         super(BleakGATTCharacteristicBlueZDBus, self).__init__(
             obj, max_write_without_response_size
diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py
index 3953fc18..1d5d16b5 100644
--- a/bleak/backends/bluezdbus/manager.py
+++ b/bleak/backends/bluezdbus/manager.py
@@ -696,7 +696,7 @@ async def get_services(
                     service.handle,
                     # "MTU" property was added in BlueZ 5.62, otherwise fall
                     # back to minimum MTU according to Bluetooth spec.
-                    char_props.get("MTU", 23) - 3,
+                    lambda: char_props.get("MTU", 23) - 3,
                 )
 
                 services.add_characteristic(char)
diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py
index d325ce92..186e7d42 100644
--- a/bleak/backends/bluezdbus/scanner.py
+++ b/bleak/backends/bluezdbus/scanner.py
@@ -237,6 +237,10 @@ def _handle_advertising_data(self, path: str, props: Device1) -> None:
             path: The D-Bus object path of the device.
             props: The D-Bus object properties of the device.
         """
+        _service_uuids = props.get("UUIDs", [])
+
+        if not self.is_allowed_uuid(_service_uuids):
+            return
 
         # Get all the information wanted to pack in the advertisement data
         _local_name = props.get("Name")
@@ -244,7 +248,6 @@ def _handle_advertising_data(self, path: str, props: Device1) -> None:
             k: bytes(v) for k, v in props.get("ManufacturerData", {}).items()
         }
         _service_data = {k: bytes(v) for k, v in props.get("ServiceData", {}).items()}
-        _service_uuids = props.get("UUIDs", [])
 
         # Get tx power data
         tx_power = props.get("TxPower")
diff --git a/bleak/backends/characteristic.py b/bleak/backends/characteristic.py
index f83917a8..eca52d5e 100644
--- a/bleak/backends/characteristic.py
+++ b/bleak/backends/characteristic.py
@@ -7,7 +7,7 @@
 """
 import abc
 import enum
-from typing import Any, List, Union
+from typing import Any, Callable, List, Union
 from uuid import UUID
 
 from ..uuids import uuidstr_to_str
@@ -30,7 +30,7 @@ class GattCharacteristicsFlags(enum.Enum):
 class BleakGATTCharacteristic(abc.ABC):
     """Interface for the Bleak representation of a GATT Characteristic"""
 
-    def __init__(self, obj: Any, max_write_without_response_size: int):
+    def __init__(self, obj: Any, max_write_without_response_size: Callable[[], int]):
         """
         Args:
             obj:
@@ -86,12 +86,30 @@ def max_write_without_response_size(self) -> int:
         Gets the maximum size in bytes that can be used for the *data* argument
         of :meth:`BleakClient.write_gatt_char()` when ``response=False``.
 
+        In rare cases, a device may take a long time to update this value, so
+        reading this property may return the default value of ``20`` and reading
+        it again after a some time may return the expected higher value.
+
+        If you *really* need to wait for a higher value, you can do something
+        like this:
+
+        .. code-block:: python
+
+            async with asyncio.timeout(10):
+                while char.max_write_without_response_size == 20:
+                    await asyncio.sleep(0.5)
+
         .. warning:: Linux quirk: For BlueZ versions < 5.62, this property
             will always return ``20``.
 
         .. versionadded:: 0.16
         """
-        return self._max_write_without_response_size
+
+        # for backwards compatibility
+        if isinstance(self._max_write_without_response_size, int):
+            return self._max_write_without_response_size
+
+        return self._max_write_without_response_size()
 
     @property
     @abc.abstractmethod
diff --git a/bleak/backends/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py
index 116c7267..4bf61711 100644
--- a/bleak/backends/corebluetooth/characteristic.py
+++ b/bleak/backends/corebluetooth/characteristic.py
@@ -6,7 +6,7 @@
 """
 
 from enum import Enum
-from typing import Dict, List, Optional, Tuple, Union
+from typing import Callable, Dict, List, Optional, Tuple, Union
 
 from CoreBluetooth import CBCharacteristic
 
@@ -59,7 +59,9 @@ class CBCharacteristicProperties(Enum):
 class BleakGATTCharacteristicCoreBluetooth(BleakGATTCharacteristic):
     """GATT Characteristic implementation for the CoreBluetooth backend"""
 
-    def __init__(self, obj: CBCharacteristic, max_write_without_response_size: int):
+    def __init__(
+        self, obj: CBCharacteristic, max_write_without_response_size: Callable[[], int]
+    ):
         super().__init__(obj, max_write_without_response_size)
         self.__descriptors: List[BleakGATTDescriptorCoreBluetooth] = []
         # self.__props = obj.properties()
diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py
index 470fc983..a682dadb 100644
--- a/bleak/backends/corebluetooth/client.py
+++ b/bleak/backends/corebluetooth/client.py
@@ -237,7 +237,7 @@ async def get_services(self, **kwargs) -> BleakGATTServiceCollection:
                 services.add_characteristic(
                     BleakGATTCharacteristicCoreBluetooth(
                         characteristic,
-                        self._peripheral.maximumWriteValueLengthForType_(
+                        lambda: self._peripheral.maximumWriteValueLengthForType_(
                             CBCharacteristicWriteWithoutResponse
                         ),
                     )
diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py
index 6452a28d..3491577d 100644
--- a/bleak/backends/corebluetooth/scanner.py
+++ b/bleak/backends/corebluetooth/scanner.py
@@ -92,6 +92,13 @@ async def start(self) -> None:
 
         def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None:
 
+            service_uuids = [
+                cb_uuid_to_str(u) for u in a.get("kCBAdvDataServiceUUIDs", [])
+            ]
+
+            if not self.is_allowed_uuid(service_uuids):
+                return
+
             # Process service data
             service_data_dict_raw = a.get("kCBAdvDataServiceData", {})
             service_data = {
@@ -108,10 +115,6 @@ def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None:
                 manufacturer_value = bytes(manufacturer_binary_data[2:])
                 manufacturer_data[manufacturer_id] = manufacturer_value
 
-            service_uuids = [
-                cb_uuid_to_str(u) for u in a.get("kCBAdvDataServiceUUIDs", [])
-            ]
-
             # set tx_power data if available
             tx_power = a.get("kCBAdvDataTxPowerLevel")
 
diff --git a/bleak/backends/p4android/characteristic.py b/bleak/backends/p4android/characteristic.py
index 562eb6ea..d9f6f191 100644
--- a/bleak/backends/p4android/characteristic.py
+++ b/bleak/backends/p4android/characteristic.py
@@ -1,4 +1,4 @@
-from typing import List, Union
+from typing import Callable, List, Union
 from uuid import UUID
 
 from ...exc import BleakError
@@ -15,7 +15,7 @@ def __init__(
         java,
         service_uuid: str,
         service_handle: int,
-        max_write_without_response_size: int,
+        max_write_without_response_size: Callable[[], int],
     ):
         super(BleakGATTCharacteristicP4Android, self).__init__(
             java, max_write_without_response_size
diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py
index 92d51075..f1bca4dd 100644
--- a/bleak/backends/p4android/client.py
+++ b/bleak/backends/p4android/client.py
@@ -273,7 +273,7 @@ async def get_services(self) -> BleakGATTServiceCollection:
                     java_characteristic,
                     service.uuid,
                     service.handle,
-                    self.__mtu - 3,
+                    lambda: self.__mtu - 3,
                 )
                 services.add_characteristic(characteristic)
 
diff --git a/bleak/backends/p4android/defs.py b/bleak/backends/p4android/defs.py
index f5245450..832f92bc 100644
--- a/bleak/backends/p4android/defs.py
+++ b/bleak/backends/p4android/defs.py
@@ -5,6 +5,7 @@
 from jnius import autoclass, cast
 
 import bleak.exc
+from bleak.uuids import normalize_uuid_16
 
 # caching constants avoids unnecessary extra use of the jni-python interface, which can be slow
 
@@ -87,4 +88,4 @@ class ScanFailed(enum.IntEnum):
     BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE: "write-without-response",
 }
 
-CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = "2902"
+CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = normalize_uuid_16(0x2902)
diff --git a/bleak/backends/p4android/scanner.py b/bleak/backends/p4android/scanner.py
index c9a6070d..fb3be74b 100644
--- a/bleak/backends/p4android/scanner.py
+++ b/bleak/backends/p4android/scanner.py
@@ -233,6 +233,9 @@ def _handle_scan_result(self, result) -> None:
         if service_uuids is not None:
             service_uuids = [service_uuid.toString() for service_uuid in service_uuids]
 
+        if not self.is_allowed_uuid(service_uuids):
+            return
+
         manufacturer_data = record.getManufacturerSpecificData()
         manufacturer_data = {
             manufacturer_data.keyAt(index): bytes(manufacturer_data.valueAt(index))
diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py
index 693b20db..eb0d71f0 100644
--- a/bleak/backends/scanner.py
+++ b/bleak/backends/scanner.py
@@ -197,32 +197,51 @@ def remove() -> None:
 
         return remove
 
-    def call_detection_callbacks(
-        self, device: BLEDevice, advertisement_data: AdvertisementData
-    ) -> None:
+    def is_allowed_uuid(self, service_uuids: Optional[List[str]]) -> bool:
         """
-        Calls all registered detection callbacks.
+        Check if the advertisement data contains any of the service UUIDs
+        matching the filter. If no filter is set, this will always return
+        ``True``.
 
-        Backend implementations should call this method when an advertisement
-        event is received from the OS.
-        """
+        Args:
+            service_uuids: The service UUIDs from the advertisement data.
 
+        Returns:
+            ``True`` if the advertisement data should be allowed or ``False``
+             if the advertisement data should be filtered out.
+        """
         # Backends will make best effort to filter out advertisements that
         # don't match the service UUIDs, but if other apps are scanning at the
         # same time or something like that, we may still receive advertisements
         # that don't match. So we need to do more filtering here to get the
         # expected behavior.
 
-        if self._service_uuids:
-            if not advertisement_data.service_uuids:
-                return
+        if not self._service_uuids:
+            # if there is no filter, everything is allowed
+            return True
+
+        if not service_uuids:
+            # if there is a filter the advertisement data doesn't contain any
+            # service UUIDs, filter it out
+            return False
 
-            for uuid in advertisement_data.service_uuids:
-                if uuid in self._service_uuids:
-                    break
-            else:
-                # if there were no matching service uuids, the don't call the callback
-                return
+        for uuid in service_uuids:
+            if uuid in self._service_uuids:
+                # match was found, keep this advertisement
+                return True
+
+        # there were no matching service uuids, filter this one out
+        return False
+
+    def call_detection_callbacks(
+        self, device: BLEDevice, advertisement_data: AdvertisementData
+    ) -> None:
+        """
+        Calls all registered detection callbacks.
+
+        Backend implementations should call this method when an advertisement
+        event is received from the OS.
+        """
 
         for callback in self._ad_callbacks.values():
             callback(device, advertisement_data)
diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py
index f72e9c67..6a576bf6 100644
--- a/bleak/backends/winrt/characteristic.py
+++ b/bleak/backends/winrt/characteristic.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 import sys
-from typing import List, Union
+from typing import Callable, List, Union
 from uuid import UUID
 
 if sys.version_info >= (3, 12):
@@ -68,7 +68,11 @@
 class BleakGATTCharacteristicWinRT(BleakGATTCharacteristic):
     """GATT Characteristic implementation for the .NET backend, implemented with WinRT"""
 
-    def __init__(self, obj: GattCharacteristic, max_write_without_response_size: int):
+    def __init__(
+        self,
+        obj: GattCharacteristic,
+        max_write_without_response_size: Callable[[], int],
+    ):
         super().__init__(obj, max_write_without_response_size)
         self.__descriptors = []
         self.__props = [
diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py
index 518fa0fe..a04ec7c5 100644
--- a/bleak/backends/winrt/client.py
+++ b/bleak/backends/winrt/client.py
@@ -218,7 +218,8 @@ def __init__(
 
         # Backend specific. WinRT objects.
         if isinstance(address_or_ble_device, BLEDevice):
-            self._device_info = address_or_ble_device.details.adv.bluetooth_address
+            data = address_or_ble_device.details
+            self._device_info = (data.adv or data.scan).bluetooth_address
         else:
             self._device_info = None
         self._requested_services = (
@@ -293,7 +294,8 @@ async def connect(self, **kwargs) -> bool:
                     self.address, f"Device with address {self.address} was not found."
                 )
 
-            self._device_info = device.details.adv.bluetooth_address
+            data = device.details
+            self._device_info = (data.adv or data.scan).bluetooth_address
 
         logger.debug("Connecting to BLE device @ %s", self.address)
 
@@ -381,8 +383,6 @@ def session_status_changed_event_handler(
             )
             loop.call_soon_threadsafe(handle_session_status_changed, args)
 
-        pdu_size_event = asyncio.Event()
-
         def max_pdu_size_changed_handler(sender: GattSession, args):
             try:
                 max_pdu_size = sender.max_pdu_size
@@ -393,7 +393,6 @@ def max_pdu_size_changed_handler(sender: GattSession, args):
                 return
 
             logger.debug("max_pdu_size_changed_handler: %d", max_pdu_size)
-            pdu_size_event.set()
 
         # Start a GATT Session to connect
         event = asyncio.Event()
@@ -490,29 +489,6 @@ def max_pdu_size_changed_handler(sender: GattSession, args):
                             cache_mode=cache_mode,
                         )
 
-                # There is a race condition where the max_pdu_size_changed event
-                # might not be received before the get_services() call completes.
-                # We could put this wait before getting services, but that would
-                # make the connection time longer. So we put it here instead and
-                # fix up the characteristics if necessary.
-                if not pdu_size_event.is_set():
-                    try:
-                        # REVISIT: Devices that don't support > default PDU size
-                        # may be punished by this timeout with a slow connection
-                        # time. We may want to consider an option to ignore this
-                        # timeout for such devices.
-                        async with async_timeout(1):
-                            await pdu_size_event.wait()
-                    except asyncio.TimeoutError:
-                        logger.debug(
-                            "max_pdu_size_changed event not received, using default"
-                        )
-
-                for char in self.services.characteristics.values():
-                    char._max_write_without_response_size = (
-                        self._session.max_pdu_size - 3
-                    )
-
                 # a connection may not be made until we request info from the
                 # device, so we have to get services before the GATT session
                 # is set to active
@@ -789,10 +765,10 @@ def dispose_on_cancel(future):
                         f"Could not get GATT descriptors for characteristic {characteristic.uuid} ({characteristic.attribute_handle})",
                     )
 
-                    # NB: max_pdu_size might not be valid at this time so we
-                    # start with default size and will update later
                     new_services.add_characteristic(
-                        BleakGATTCharacteristicWinRT(characteristic, 20)
+                        BleakGATTCharacteristicWinRT(
+                            characteristic, lambda: self._session.max_pdu_size - 3
+                        )
                     )
 
                     for descriptor in descriptors:
diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py
index 651fbc75..723ae1fe 100644
--- a/bleak/backends/winrt/scanner.py
+++ b/bleak/backends/winrt/scanner.py
@@ -50,7 +50,7 @@ class _RawAdvData(NamedTuple):
     advertising data like other platforms, so se have to do it ourselves.
     """
 
-    adv: BluetoothLEAdvertisementReceivedEventArgs
+    adv: Optional[BluetoothLEAdvertisementReceivedEventArgs]
     """
     The advertisement data received from the BluetoothLEAdvertisementWatcher.Received event.
     """
@@ -188,6 +188,9 @@ def _received_handler(
                 data = bytes(section.data)
                 service_data[str(UUID(bytes=bytes(data[15::-1])))] = data[16:]
 
+        if not self.is_allowed_uuid(uuids):
+            return
+
         # Use the BLEDevice to populate all the fields for the advertisement data to return
         advertisement_data = AdvertisementData(
             local_name=local_name,
@@ -219,7 +222,7 @@ async def start(self) -> None:
 
         # Callbacks for WinRT async methods will never happen in STA mode if
         # there is nothing pumping a Windows message loop.
-        assert_mta()
+        await assert_mta()
 
         # start with fresh list of discovered devices
         self.seen_devices = {}
diff --git a/bleak/backends/winrt/util.py b/bleak/backends/winrt/util.py
index 50fac389..89a18c2d 100644
--- a/bleak/backends/winrt/util.py
+++ b/bleak/backends/winrt/util.py
@@ -1,9 +1,24 @@
+import asyncio
 import ctypes
+import sys
+from ctypes import wintypes
 from enum import IntEnum
 from typing import Tuple
 
 from ...exc import BleakError
 
+if sys.version_info < (3, 11):
+    from async_timeout import timeout as async_timeout
+else:
+    from asyncio import timeout as async_timeout
+
+
+def _check_result(result, func, args):
+    if not result:
+        raise ctypes.WinError()
+
+    return args
+
 
 def _check_hresult(result, func, args):
     if result:
@@ -12,6 +27,26 @@ def _check_hresult(result, func, args):
     return args
 
 
+# not defined in wintypes
+_UINT_PTR = wintypes.WPARAM
+
+# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-timerproc
+_TIMERPROC = ctypes.WINFUNCTYPE(
+    None, wintypes.HWND, _UINT_PTR, wintypes.UINT, wintypes.DWORD
+)
+
+# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-settimer
+_SetTimer = ctypes.windll.user32.SetTimer
+_SetTimer.restype = _UINT_PTR
+_SetTimer.argtypes = [wintypes.HWND, _UINT_PTR, wintypes.UINT, _TIMERPROC]
+_SetTimer.errcheck = _check_result
+
+# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-killtimer
+_KillTimer = ctypes.windll.user32.KillTimer
+_KillTimer.restype = wintypes.BOOL
+_KillTimer.argtypes = [wintypes.HWND, wintypes.UINT]
+
+
 # https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cogetapartmenttype
 _CoGetApartmentType = ctypes.windll.ole32.CoGetApartmentType
 _CoGetApartmentType.restype = ctypes.c_int
@@ -60,28 +95,71 @@ def _get_apartment_type() -> Tuple[_AptType, _AptQualifierType]:
     return _AptType(api_type.value), _AptQualifierType(api_type_qualifier.value)
 
 
-def assert_mta() -> None:
+async def assert_mta() -> None:
     """
     Asserts that the current apartment type is MTA.
 
     Raises:
-        BleakError: If the current apartment type is not MTA.
+        BleakError:
+            If the current apartment type is not MTA and there is no Windows
+            message loop running.
 
     .. versionadded:: 0.22
+
+    .. versionchanged:: 0.22.2
+
+        Function is now async and will not raise if the current apartment type
+        is STA and the Windows message loop is running.
     """
     if hasattr(allow_sta, "_allowed"):
         return
 
     try:
         apt_type, _ = _get_apartment_type()
-        if apt_type != _AptType.MTA:
-            raise BleakError(
-                f"The current thread apartment type is not MTA: {apt_type.name}. Beware of packages like pywin32 that may change the apartment type implicitly."
-            )
     except OSError as e:
         # All is OK if not initialized yet. WinRT will initialize it.
-        if e.winerror != _CO_E_NOTINITIALIZED:
-            raise
+        if e.winerror == _CO_E_NOTINITIALIZED:
+            return
+
+        raise
+
+    if apt_type == _AptType.MTA:
+        # if we get here, WinRT probably set the apartment type to MTA and all
+        # is well, we don't need to check again
+        setattr(allow_sta, "_allowed", True)
+        return
+
+    event = asyncio.Event()
+
+    def wait_event(*_):
+        event.set()
+
+    # have to keep a reference to the callback or it will be garbage collected
+    # before it is called
+    callback = _TIMERPROC(wait_event)
+
+    # set a timer to see if we get a callback to ensure the windows event loop
+    # is running
+    timer = _SetTimer(None, 1, 0, callback)
+
+    try:
+        async with async_timeout(0.5):
+            await event.wait()
+    except asyncio.TimeoutError:
+        raise BleakError(
+            "Thread is configured for Windows GUI but callbacks are not working."
+            + (
+                " Suspect unwanted side effects from importing 'pythoncom'."
+                if "pythoncom" in sys.modules
+                else ""
+            )
+        )
+    else:
+        # if the windows event loop is running, we assume it is going to keep
+        # running and we don't need to check again
+        setattr(allow_sta, "_allowed", True)
+    finally:
+        _KillTimer(None, timer)
 
 
 def allow_sta():
@@ -115,7 +193,12 @@ def uninitialize_sta():
 
     .. versionadded:: 0.22
     """
+
     try:
-        assert_mta()
-    except BleakError:
+        _get_apartment_type()
+    except OSError as e:
+        # All is OK if not initialized yet. WinRT will initialize it.
+        if e.winerror == _CO_E_NOTINITIALIZED:
+            return
+    else:
         ctypes.windll.ole32.CoUninitialize()
diff --git a/bleak/uuids.py b/bleak/uuids.py
index ed92698c..2730e506 100644
--- a/bleak/uuids.py
+++ b/bleak/uuids.py
@@ -1214,15 +1214,15 @@ def normalize_uuid_str(uuid: str) -> str:
 
         # 16-bit
         uuid1 = normalize_uuid_str("1234")
-        # uuid1 == "00001234-1000-8000-00805f9b34fb"
+        # uuid1 == "00001234-0000-1000-8000-00805f9b34fb"
 
         # 32-bit
         uuid2 = normalize_uuid_str("12345678")
-        # uuid2 == "12345678-1000-8000-00805f9b34fb"
+        # uuid2 == "12345678-0000-1000-8000-00805f9b34fb"
 
         # 128-bit
-        uuid3 = normalize_uuid_str("12345678-1234-1234-1234567890ABC")
-        # uuid3 == "12345678-1234-1234-1234567890abc"
+        uuid3 = normalize_uuid_str("12345678-0000-1234-1234-1234567890ABC")
+        # uuid3 == "12345678-0000-1234-1234-1234567890abc"
 
     .. versionadded:: 0.20
     .. versionchanged:: 0.21
@@ -1245,12 +1245,12 @@ def normalize_uuid_16(uuid: int) -> str:
     Normaizes a 16-bit integer UUID to the format used by Bleak.
 
     Returns:
-        128-bit UUID as string with the format ``"0000xxxx-1000-8000-00805f9b34fb"``.
+        128-bit UUID as string with the format ``"0000xxxx-0000-1000-8000-00805f9b34fb"``.
 
     Example::
 
         uuid = normalize_uuid_16(0x1234)
-        # uuid == "00001234-1000-8000-00805f9b34fb"
+        # uuid == "00001234-0000-1000-8000-00805f9b34fb"
 
     .. versionadded:: 0.21
     """
@@ -1262,12 +1262,12 @@ def normalize_uuid_32(uuid: int) -> str:
     Normaizes a 32-bit integer UUID to the format used by Bleak.
 
     Returns:
-        128-bit UUID as string with the format ``"xxxxxxxx-1000-8000-00805f9b34fb"``.
+        128-bit UUID as string with the format ``"xxxxxxxx-0000-1000-8000-00805f9b34fb"``.
 
     Example::
 
         uuid = normalize_uuid_32(0x12345678)
-        # uuid == "12345678-1000-8000-00805f9b34fb"
+        # uuid == "12345678-0000-1000-8000-00805f9b34fb"
 
     .. versionadded:: 0.21
     """
diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst
index 514eac05..5a2ec28d 100644
--- a/docs/troubleshooting.rst
+++ b/docs/troubleshooting.rst
@@ -8,6 +8,109 @@ When things don't seem to be working right, here are some things to try.
 Common Mistakes
 ---------------
 
+Calling ``asyncio.run()`` more than once
+========================================
+
+Bleak requires the same asyncio run loop to be used for all of its operations.
+And it requires the loop to always be running because there are background tasks
+that need to always be running. Therefore, make sure you only call ``asyncio.run()``
+once at the start of your program. **Your program will not work correctly if you
+call it more than once.** Even if it seems like it is working, crashes and other
+problems will occur eventually.
+
+DON'T!
+
+.. code-block:: python
+
+    async def scan():
+        return await BleakScanner.find_device_by_name("My Device")
+
+    async def connect(device):
+        async with BleakClient(device) as client:
+            data = await client.read_gatt_char(MY_CHAR_UUID)
+            print("received:" data)
+
+    # Do not wrap each function call in asyncio.run() like this!
+    device = asyncio.run(scan())
+    if not device:
+        print("Device not found")
+    else:
+        asyncio.run(connect(device))
+
+
+DO!
+
+.. code-block:: python
+
+    async def scan():
+        return await BleakScanner.find_device_by_name("My Device")
+
+    async def connect(device):
+        async with BleakClient(device) as client:
+            data = await client.read_gatt_char(MY_CHAR_UUID)
+            print("received:" data)
+
+    # Do have one async main function that does everything.
+    async def main():
+        device = await scan()
+        if not device:
+            print("Device not found")
+            return
+
+        await connect(device)
+
+    asyncio.run(main())
+
+
+DON'T!
+
+.. code-block:: python
+
+    async def scan_and_connect():
+        device = await BleakScanner.find_device_by_name("My Device")
+        if not device:
+            print("Device not found")
+            return
+
+        async with BleakClient(device) as client:
+            data = await client.read_gatt_char(MY_CHAR_UUID)
+            print("received:" data)
+
+
+    while True:
+        # Don't call asyncio.run() multiple times like this!
+        asyncio.run(scan_and_connect())
+        # Never use blocking sleep in an asyncio programs!
+        time.sleep(5)
+
+
+DO!
+
+.. code-block:: python
+
+    async def scan_and_connect():
+        device = await BleakScanner.find_device_by_name("My Device")
+        if not device:
+            print("Device not found")
+            return
+
+        async with BleakClient(device) as client:
+            data = await client.read_gatt_char(MY_CHAR_UUID)
+            print("received:" data)
+
+    # Do have one async main function that does everything.
+    async def main():
+        while True:
+            await scan_and_connect()
+            # Do use asyncio.sleep() in an asyncio program.
+            await asyncio.sleep(5)
+
+    asyncio.run(main())
+
+
+Naming your script ``bleak.py``
+===============================
+
 Many people name their first script ``bleak.py``. This causes the script to
 crash with an ``ImportError`` similar to::
 
@@ -74,15 +177,32 @@ Not working when threading model is STA
 
 Packages like ``pywin32`` and it's subsidiaries have an unfortunate side effect
 of initializing the threading model to Single Threaded Apartment (STA) when
-imported. This causes async WinRT functions to never complete. because there
-isn't a message loop running. Bleak needs to run in a Multi Threaded Apartment
-(MTA) instead (this happens automatically on the first WinRT call).
+imported. This causes async WinRT functions to never complete if Bleak is being
+used in a console application (no Windows graphical user interface). This is
+because there isn't a Windows message loop running to handle async callbacks.
+Bleak, when used in a console application, needs to run in a Multi Threaded
+Apartment (MTA) instead (this happens automatically on the first WinRT call).
 
 Bleak should detect this and raise an exception with a message similar to::
 
-    The current thread apartment type is not MTA: STA.
+    Thread is configured for Windows GUI but callbacks are not working.
+
+You can tell a ``pywin32`` package caused the issue by checking for
+``"pythoncom" in sys.modules``. If it is there, then likely it triggered the
+problem. You can avoid this by setting ``sys.coinit_flags = 0`` before importing
+any package that indirectly imports ``pythoncom``. This will cause ``pythoncom``
+to use the default threading model (MTA) instead of STA.
+
+Example::
+
+    import sys
+    sys.coinit_flags = 0  # 0 means MTA
+
+    import win32com  # or any other package that causes the issue
 
-To work around this, you can use one of the utility functions provided by Bleak.
+
+If the issue was caused by something other than the ``pythoncom`` module, there
+are a couple of other helper functions you can try.
 
 If your program has a graphical user interface and the UI framework *and* it is
 properly integrated with asyncio *and* Bleak is not running on a background
@@ -98,14 +218,20 @@ thread then call ``allow_sta()`` before calling any other Bleak APis::
         # can safely ignore
         pass
 
-The more typical case, though, is that some library has imported something like
-``pywin32`` which breaks Bleak. In this case, you can uninitialize the threading
-model like this::
+The more typical case, though, is that some library has imported something similar
+to ``pythoncom`` with the same unwanted side effect of initializing the main
+thread of a console application to STA. In this case, you can uninitialize the
+threading model like this::
+
+    import naughty_module  # this sets current thread to STA :-(
 
-    import win32com  # this sets current thread to STA :-(
-    from bleak.backends.winrt.util import uninitialize_sta
+    try:
+        from bleak.backends.winrt.util import uninitialize_sta
 
-    uninitialize_sta()  # undo the unwanted side effect
+        uninitialize_sta()  # undo the unwanted side effect
+    except ImportError:
+        # not Windows, so no problem
+        pass
 
 
 --------------
diff --git a/examples/discover.py b/examples/discover.py
index c16f5bd1..6e8e99f5 100644
--- a/examples/discover.py
+++ b/examples/discover.py
@@ -18,7 +18,9 @@ async def main(args: argparse.Namespace):
     print("scanning for 5 seconds, please wait...")
 
     devices = await BleakScanner.discover(
-        return_adv=True, cb=dict(use_bdaddr=args.macos_use_bdaddr)
+        return_adv=True,
+        service_uuids=args.services,
+        cb=dict(use_bdaddr=args.macos_use_bdaddr),
     )
 
     for d, a in devices.values():
@@ -31,6 +33,13 @@ async def main(args: argparse.Namespace):
 if __name__ == "__main__":
     parser = argparse.ArgumentParser()
 
+    parser.add_argument(
+        "--services",
+        metavar="<uuid>",
+        nargs="*",
+        help="UUIDs of one or more services to filter for",
+    )
+
     parser.add_argument(
         "--macos-use-bdaddr",
         action="store_true",
diff --git a/poetry.lock b/poetry.lock
index c3278b39..71369f90 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -22,23 +22,6 @@ files = [
     {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
 ]
 
-[[package]]
-name = "attrs"
-version = "22.1.0"
-description = "Classes Without Boilerplate"
-optional = false
-python-versions = ">=3.5"
-files = [
-    {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
-    {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
-]
-
-[package.extras]
-dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
-docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
-tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
-tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
-
 [[package]]
 name = "Babel"
 version = "2.10.3"
@@ -288,6 +271,20 @@ files = [
     {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
 ]
 
+[[package]]
+name = "exceptiongroup"
+version = "1.2.1"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+    {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
 [[package]]
 name = "flake8"
 version = "5.0.4"
@@ -497,30 +494,19 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock
 
 [[package]]
 name = "pluggy"
-version = "1.0.0"
+version = "1.5.0"
 description = "plugin and hook calling mechanisms for python"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
 files = [
-    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
-    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+    {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+    {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
 ]
 
 [package.extras]
 dev = ["pre-commit", "tox"]
 testing = ["pytest", "pytest-benchmark"]
 
-[[package]]
-name = "py"
-version = "1.11.0"
-description = "library with cross-python path, ini-parsing, io, code, log facilities"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-files = [
-    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
-    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
-]
-
 [[package]]
 name = "pycodestyle"
 version = "2.9.1"
@@ -630,43 +616,43 @@ pyobjc-core = ">=10.0"
 
 [[package]]
 name = "pytest"
-version = "7.1.3"
+version = "8.2.1"
 description = "pytest: simple powerful testing with Python"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
-    {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
+    {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
+    {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
 ]
 
 [package.dependencies]
-attrs = ">=19.2.0"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
 iniconfig = "*"
 packaging = "*"
-pluggy = ">=0.12,<2.0"
-py = ">=1.8.2"
-tomli = ">=1.0.0"
+pluggy = ">=1.5,<2.0"
+tomli = {version = ">=1", markers = "python_version < \"3.11\""}
 
 [package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
 [[package]]
 name = "pytest-asyncio"
-version = "0.19.0"
+version = "0.23.7"
 description = "Pytest support for asyncio"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"},
-    {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"},
+    {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
+    {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
 ]
 
 [package.dependencies]
-pytest = ">=6.1.0"
+pytest = ">=7.0.0,<9"
 
 [package.extras]
-testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
 
 [[package]]
 name = "pytest-cov"
@@ -699,13 +685,13 @@ files = [
 
 [[package]]
 name = "requests"
-version = "2.31.0"
+version = "2.32.0"
 description = "Python HTTP for Humans."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
-    {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+    {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"},
+    {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"},
 ]
 
 [package.dependencies]
@@ -950,7 +936,6 @@ files = [
     {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp39-cp39-win32.whl", hash = "sha256:dffff7e6801b8e69e694b36fe1d147094fb6ac29ce54fd3ca3e52ab417473cc4"},
     {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:62bae806ecdf3021e1ec685d5a44012657c0961ca2027eeb1c37864f53577e51"},
     {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:7f3b102e9b4bea1915cc922b571e0c226956c161102d228ec1788e3caf4e226d"},
-    {file = "winrt_windows_devices_bluetooth-2.0.1.tar.gz", hash = "sha256:c91b3f54bfe1ed7e1e597566b83a625d32efe397b21473668046ccb4b57f5a28"},
 ]
 
 [package.dependencies]
@@ -978,7 +963,6 @@ files = [
     {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp39-cp39-win32.whl", hash = "sha256:86d11fd5c055f76eefac7f6cc02450832811503b83280e26a83613afe1d17c92"},
     {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:c8495ce12fda8fce3da130664917eb199d19ca1ebf7d5ab996f5df584b5e3a1f"},
     {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:0e91160a98e5b0fffae196982b5670e678ac919a6e14eb7e9798fdcbff45f8d2"},
-    {file = "winrt_windows_devices_bluetooth_advertisement-2.0.1.tar.gz", hash = "sha256:130e6238a1897bfef98a711cdb1b02694fa0e18eb67d8fd4019a64a53685b331"},
 ]
 
 [package.dependencies]
@@ -1006,7 +990,6 @@ files = [
     {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp39-cp39-win32.whl", hash = "sha256:3e2a54db384dcf05265a855a2548e2abd9b7726c8ec4b9ad06059606c5d90409"},
     {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:2bdbb55d4bef15c762a5d5b4e27b534146ec6580075ed9cc681e75e6ff0d5a97"},
     {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:01e74c76d4f16b4490d78c8c7509f2570c843366c1c6bf196a5b729520a31258"},
-    {file = "winrt_windows_devices_bluetooth_genericattributeprofile-2.0.1.tar.gz", hash = "sha256:69d7dabd53fbf9acdc2d206def60f5c9777416a9d6911c3420be700aaff4e492"},
 ]
 
 [package.dependencies]
@@ -1034,7 +1017,6 @@ files = [
     {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp39-cp39-win32.whl", hash = "sha256:9301f5e00bd2562b063e0f6e0de6f0596b7fb3eabc443bd7e115772de6cc08f9"},
     {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:9999d93ae9441d35c564d498bb4d6767b593254a92b7c1559058a7450a0c304e"},
     {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:504ca45a9b90387a2f4f727dbbeefcf79beb013ac7a29081bb14c8ab13e10367"},
-    {file = "winrt_windows_devices_enumeration-2.0.1.tar.gz", hash = "sha256:ed227dd22ece253db913de24e4fc5194d9f3272e2a5959a2450ae79e81bf7949"},
 ]
 
 [package.dependencies]
@@ -1062,7 +1044,6 @@ files = [
     {file = "winrt_Windows.Foundation-2.0.1-cp39-cp39-win32.whl", hash = "sha256:7abbf10666d6da5dbfb6a47125786a05dac267731a3d38feb8faddade9bf1151"},
     {file = "winrt_Windows.Foundation-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:aab18ad12de63a353ab1847aff3216ba4e5499e328da5edcb72c8007da6bdb02"},
     {file = "winrt_Windows.Foundation-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:bde9ecfc1c75410d669ee3124a84ba101d5a8ab1911807ad227658624fc22ffb"},
-    {file = "winrt_windows_foundation-2.0.1.tar.gz", hash = "sha256:6e4da10cff652ac17740753c38ebe69565f5f970f60100106469b2e004ef312c"},
 ]
 
 [package.dependencies]
@@ -1090,7 +1071,6 @@ files = [
     {file = "winrt_Windows.Foundation.Collections-2.0.1-cp39-cp39-win32.whl", hash = "sha256:c26ab7b3342669dc09be62db5c5434e7194fb6eb1ec5b03fba1163f6b3e7b843"},
     {file = "winrt_Windows.Foundation.Collections-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:2f9bc7e28f3ade1c1f3113939dbf630bfef5e3c3018c039a404d7e4d39aae4cb"},
     {file = "winrt_Windows.Foundation.Collections-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:1f3e76f3298bec3938d94e4857c29af9776ec78112bdd09bb7794f06fd38bb13"},
-    {file = "winrt_windows_foundation_collections-2.0.1.tar.gz", hash = "sha256:7d18955f161ba27d785c8fe2ef340f338b6edd2c5226fe2b005840e2a855e708"},
 ]
 
 [package.dependencies]
@@ -1118,7 +1098,6 @@ files = [
     {file = "winrt_Windows.Storage.Streams-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f6dec418ad0118c258a1b2999fc8d4fc0d9575e6353a75a242ff8cc63c9b2146"},
     {file = "winrt_Windows.Storage.Streams-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:9fbc40f600ab44a45cda47b698bd8e494e80e221446a5958c4d8d59a8d46f117"},
     {file = "winrt_Windows.Storage.Streams-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:08059774c6d49d195ce00c3802d19364f418a6f3e42b94373621551792d2da60"},
-    {file = "winrt_windows_storage_streams-2.0.1.tar.gz", hash = "sha256:3de8351ed3a9cfcfd1d028ce97ffe90bb95744f906eef025b06e7f4431943ee6"},
 ]
 
 [package.dependencies]
@@ -1145,4 +1124,4 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.8,<3.13"
-content-hash = "65f1bbdea293cf8fef2b3531e7f28b9aff5b62df6c9bbf4a290d9cdfe5f8884d"
+content-hash = "1c5a0ca2a13af74c3aecab397053176e19054a14bcbb031d89c8413acaf8a468"
diff --git a/pyproject.toml b/pyproject.toml
index cf027c0e..092f7ffe 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "bleak"
-version = "0.22.1"
+version = "0.22.2"
 description = "Bluetooth Low Energy platform Agnostic Klient"
 authors = ["Henrik Blidh <henrik.blidh@nedomkull.com>"]
 license = "MIT"
@@ -49,8 +49,8 @@ flake8 = "^5.0.0"
 isort = "^5.13.2"
 
 [tool.poetry.group.test.dependencies]
-pytest = "^7.0.0"
-pytest-asyncio = "^0.19.0"
+pytest = "^8.2.1"
+pytest-asyncio = "^0.23.7"
 pytest-cov = "^3.0.0 "
 
 [build-system]


Test patch:
diff --git a/tests/bleak/backends/winrt/test_utils.py b/tests/bleak/backends/winrt/test_utils.py
new file mode 100644
index 00000000..3c90dbcc
--- /dev/null
+++ b/tests/bleak/backends/winrt/test_utils.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+
+"""Tests for `bleak.backends.winrt.util` package."""
+
+import sys
+
+import pytest
+
+if not sys.platform.startswith("win"):
+    pytest.skip("skipping windows-only tests", allow_module_level=True)
+
+from ctypes import windll, wintypes
+
+from bleak.backends.winrt.util import (
+    _check_hresult,
+    allow_sta,
+    assert_mta,
+    uninitialize_sta,
+)
+from bleak.exc import BleakError
+
+# https://learn.microsoft.com/en-us/windows/win32/api/objbase/ne-objbase-coinit
+COINIT_MULTITHREADED = 0x0
+COINIT_APARTMENTTHREADED = 0x2
+
+# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex
+_CoInitializeEx = windll.ole32.CoInitializeEx
+_CoInitializeEx.restype = wintypes.LONG
+_CoInitializeEx.argtypes = [wintypes.LPVOID, wintypes.DWORD]
+_CoInitializeEx.errcheck = _check_hresult
+
+# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-couninitialize
+_CoUninitialize = windll.ole32.CoUninitialize
+_CoUninitialize.restype = None
+_CoUninitialize.argtypes = []
+
+
+@pytest.fixture(autouse=True)
+def run_around_tests():
+    # reset global state
+    try:
+        delattr(allow_sta, "_allowed")
+    except AttributeError:
+        pass
+
+    yield
+
+
+@pytest.mark.asyncio
+async def test_assert_mta_no_init():
+    """Test device_path_from_characteristic_path."""
+
+    await assert_mta()
+
+
+@pytest.mark.asyncio
+async def test_assert_mta_init_mta():
+    """Test device_path_from_characteristic_path."""
+
+    _CoInitializeEx(None, COINIT_MULTITHREADED)
+
+    try:
+        await assert_mta()
+        assert hasattr(allow_sta, "_allowed")
+    finally:
+        _CoUninitialize()
+
+
+@pytest.mark.asyncio
+async def test_assert_mta_init_sta():
+    """Test device_path_from_characteristic_path."""
+
+    _CoInitializeEx(None, COINIT_APARTMENTTHREADED)
+
+    try:
+        with pytest.raises(
+            BleakError,
+            match="Thread is configured for Windows GUI but callbacks are not working.",
+        ):
+            await assert_mta()
+    finally:
+        _CoUninitialize()
+
+
+@pytest.mark.asyncio
+async def test_uninitialize_sta():
+    """Test device_path_from_characteristic_path."""
+
+    _CoInitializeEx(None, COINIT_APARTMENTTHREADED)
+    uninitialize_sta()
+
+    await assert_mta()