diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 33e3fb6e6..00060d471 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,10 +6,10 @@ Before you submit a pull request, check that it meets these guidelines: 1. If the pull request adds functionality, the docs should be updated. 2. Modify the `CHANGELOG.rst`, describing your changes as is specified by the guidelines in that document. -3. The pull request should work for Python 3.8+ on the following platforms: +3. The pull request should work for Python 3.9+ on the following platforms: - Windows 10, version 16299 (Fall Creators Update) and greater - - Linux distributions with BlueZ >= 5.43 - - OS X / macOS >= 10.11 + - Linux distributions with BlueZ >= 5.55 + - OS X / macOS >= 10.13 4. Squash all your commits on your PR branch, if the commits are not solving different problems and you are committing them in the same PR. In that case, consider making several PRs instead. diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 22a5b50a1..5ef2053d3 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -3,8 +3,10 @@ name: Build and Test on: push: branches: [ master, develop ] + paths: [ 'bleak/**', 'tests/**' ] pull_request: branches: [ master, develop ] + paths: [ 'bleak/**', 'tests/**' ] jobs: build_desktop: @@ -14,7 +16,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -31,7 +33,7 @@ jobs: run: pipx run poetry install --only main,test - name: Test with pytest run: | - pipx run poetry run pytest tests --junitxml=junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml --cov=com --cov-report=xml --cov-report=html + pipx run poetry run pytest -v tests --junitxml=junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml --cov=com --cov-report=xml --cov-report=html - name: Upload pytest test results uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/format_and_lint.yml b/.github/workflows/format_and_lint.yml index 2108e846e..8445dc936 100644 --- a/.github/workflows/format_and_lint.yml +++ b/.github/workflows/format_and_lint.yml @@ -17,7 +17,7 @@ jobs: - name: Install development dependencies run: pipx run poetry install --only docs,lint - name: Check import sort with isort - run: pipx run poetry run isort . --check --diff + run: pipx run poetry run isort {.,docs} --check --diff - name: Check code formatting with black run: pipx run poetry run black . --check --diff - name: Lint with flake8 diff --git a/.github/workflows/stale_issues.yml b/.github/workflows/stale_issues.yml new file mode 100644 index 000000000..9cd840cbb --- /dev/null +++ b/.github/workflows/stale_issues.yml @@ -0,0 +1,16 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '37 1 * * 0' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 500 + days-before-close: 100 + any-of-labels: '3rd party issue, more info required' + stale-issue-message: This issue has been automatically marked as stale because it has not had any activity for 500 days. It will be closed in 100 days if no further activity occurs. + close-issue-message: This issue has been automatically closed because it has not had any activity for 100 days after being marked as stale. diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c717a663..49cace7cb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,6 @@ "black-formatter.importStrategy": "fromEnvironment", "isort.importStrategy": "fromEnvironment", "isort.args":["--profile", "black"], - "flake8.importStrategy": "fromEnvironment" + "flake8.importStrategy": "fromEnvironment", + "python.analysis.typeCheckingMode": "strict" } diff --git a/AUTHORS.rst b/AUTHORS.rst index b6eca00d2..ac6f7769a 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -25,6 +25,8 @@ Contributors * JP Hutchins * Bram Duvigneau +And many others who did not wish to be named here. + Sponsors -------- diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c360d485..bdffec57e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,56 @@ and this project adheres to `Semantic Versioning =2.0``. Merged #1718. +* Log to stderr instead of stdout when ``BLEAK_LOGGING`` is enabled. Merged #1709. +* Updated ``winrt`` backend to use PyWinRT >= 3.1. +* Changed return type of ``connect()``, ``disconnect()``, ``pair()`` and ``unpair()`` methods to ``None``. +* Moved backend-specific arg types to new ``bleak.args`` sub-package. +* ``BLEDevice.name`` will now return ``None`` instead of the address when the name is not available. Merged #1762. +* Deprecated ``protection_level`` kwarg for pairing in WinRT backend. Merged #1770. + +Fixed +----- +* Fixed resolvable private address not updated after connecting in BlueZ backend. Fixes #1737. +* Fixed possible ``KeyError`` when getting services in BlueZ backend. Fixes #1435. +* Fix D-Bus connection leak when connecting to a device fails in BlueZ backend. Fixes #1698. +* Fixed possible deadlock when connecting on WinRT backend when device is already connected. Fixes #1757. +* Fixed getting notifications from devices connected to BLE adapters with index >9 (hci10, hci11, ...). Merged #1744. +* Fixed ATT error code 15 description to "Insufficient Encryption". Merged #1746. + +Removed +------- +* Removed support for Python 3.8. The minimum supported version is now Python 3.9. +* Removed deprecated parameters, properties and methods. +* Removed support for macOS < 10.13. +* Removed support for BlueZ < 5.55. + `0.22.3`_ (2024-10-05) ====================== @@ -484,6 +534,7 @@ Changed * Replaced usage of deprecated ``@abc.abstractproperty``. * Use ``asyncio.get_running_loop()`` instead of ``asyncio.get_event_loop()``. * Changed "service is already present" exception to logged error in BlueZ backend. Merged #622. +* WinRT backend no longer waits for GATT session to close on disconnect. Fixes #1759. Removed ------- @@ -1047,7 +1098,9 @@ Fixed * Bleak created. -.. _Unreleased: https://github.com/hbldh/bleak/compare/v0.22.3...develop +.. _Unreleased: https://github.com/hbldh/bleak/compare/v1.0.1...develop +.. _1.0.1: https://github.com/hbldh/bleak/compare/v1.0.0...v1.0.1 +.. _1.0.0: https://github.com/hbldh/bleak/compare/v0.22.3...v1.0.0 .. _0.22.3: https://github.com/hbldh/bleak/compare/v0.22.2...v0.22.3 .. _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 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9f9f26925..dae1ff871 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -102,10 +102,10 @@ Before you submit a pull request, check that it meets these guidelines: 1. If the pull request adds functionality, the docs should be updated. 2. Modify the ``CHANGELOG.rst``, describing your changes as is specified by the guidelines in that document. -3. The pull request should work for Python 3.8+ on the following platforms: +3. The pull request should work for Python 3.9+ on the following platforms: - Windows 10, version 16299 (Fall Creators Update) and greater - - Linux distributions with BlueZ >= 5.43 - - OS X / macOS >= 10.11 + - Linux distributions with BlueZ >= 5.55 + - OS X / macOS >= 10.13 4. Squash all your commits on your PR branch, if the commits are not solving different problems and you are committing them in the same PR. In that case, consider making several PRs instead. diff --git a/README.rst b/README.rst index 2c5d05501..da1fe095b 100644 --- a/README.rst +++ b/README.rst @@ -46,8 +46,8 @@ Features -------- * Supports Windows 10, version 16299 (Fall Creators Update) or greater -* Supports Linux distributions with BlueZ >= 5.43 -* OS X/macOS support via Core Bluetooth API, from at least OS X version 10.11 +* Supports Linux distributions with BlueZ >= 5.55 +* OS X/macOS support via Core Bluetooth API, from at least OS X version 10.13 * Android backend compatible with python-for-android Bleak supports reading, writing and getting notifications from diff --git a/bleak/__init__.py b/bleak/__init__.py index 5e23d513e..f02e981e4 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -14,25 +14,9 @@ import os import sys import uuid +from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable from types import TracebackType -from typing import ( - TYPE_CHECKING, - AsyncGenerator, - Awaitable, - Callable, - Dict, - Iterable, - List, - Literal, - Optional, - Set, - Tuple, - Type, - TypedDict, - Union, - overload, -) -from warnings import warn +from typing import Any, Literal, Optional, TypedDict, Union, cast, overload if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -41,36 +25,34 @@ if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout - from typing_extensions import Unpack + from typing_extensions import Never, Self, Unpack, assert_never else: from asyncio import timeout as async_timeout - from typing import Unpack - -from .backends.characteristic import BleakGATTCharacteristic -from .backends.client import BaseBleakClient, get_platform_client_backend_type -from .backends.device import BLEDevice -from .backends.scanner import ( + from typing import Never, Self, Unpack, assert_never + +from bleak.args.bluez import BlueZScannerArgs +from bleak.args.corebluetooth import CBScannerArgs, CBStartNotifyArgs +from bleak.args.winrt import WinRTClientArgs +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, AdvertisementDataFilter, BaseBleakScanner, get_platform_scanner_backend_type, ) -from .backends.service import BleakGATTServiceCollection -from .exc import BleakCharacteristicNotFoundError, BleakError -from .uuids import normalize_uuid_str - -if TYPE_CHECKING: - from .backends.bluezdbus.scanner import BlueZScannerArgs - from .backends.corebluetooth.scanner import CBScannerArgs - from .backends.winrt.client import WinRTClientArgs - +from bleak.backends.service import BleakGATTServiceCollection +from bleak.exc import BleakCharacteristicNotFoundError, BleakError +from bleak.uuids import normalize_uuid_str _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) if bool(os.environ.get("BLEAK_LOGGING", False)): FORMAT = "%(asctime)-15s %(name)-8s %(threadName)s %(levelname)s: %(message)s" - handler = logging.StreamHandler(sys.stdout) + handler = logging.StreamHandler(sys.stderr) handler.setLevel(logging.DEBUG) handler.setFormatter(logging.Formatter(fmt=FORMAT)) _logger.addHandler(handler) @@ -78,7 +60,7 @@ # prevent tasks from being garbage collected -_background_tasks: Set[asyncio.Task] = set() +_background_tasks = set[asyncio.Task[None]]() class BleakScanner: @@ -133,13 +115,13 @@ class BleakScanner: def __init__( self, detection_callback: Optional[AdvertisementDataCallback] = None, - service_uuids: Optional[List[str]] = None, + service_uuids: Optional[list[str]] = None, scanning_mode: Literal["active", "passive"] = "active", *, bluez: BlueZScannerArgs = {}, cb: CBScannerArgs = {}, - backend: Optional[Type[BaseBleakScanner]] = None, - **kwargs, + backend: Optional[type[BaseBleakScanner]] = None, + **kwargs: Any, ) -> None: PlatformBleakScanner = ( get_platform_scanner_backend_type() if backend is None else backend @@ -148,56 +130,24 @@ def __init__( self._backend = PlatformBleakScanner( detection_callback, service_uuids, - scanning_mode, + scanning_mode, # type: ignore bluez=bluez, cb=cb, **kwargs, - ) + ) # type: ignore - async def __aenter__(self) -> BleakScanner: + async def __aenter__(self) -> Self: await self._backend.start() return self async def __aexit__( self, - exc_type: Type[BaseException], + exc_type: type[BaseException], exc_val: BaseException, exc_tb: TracebackType, ) -> None: await self._backend.stop() - def register_detection_callback( - self, callback: Optional[AdvertisementDataCallback] - ) -> None: - """ - Register a callback that is called when a device is discovered or has a property changed. - - .. deprecated:: 0.17.0 - This method will be removed in a future version of Bleak. Pass - the callback directly to the :class:`BleakScanner` constructor instead. - - Args: - callback: A function, coroutine or ``None``. - - - """ - warn( - "This method will be removed in a future version of Bleak. Use the detection_callback of the BleakScanner constructor instead.", - FutureWarning, - stacklevel=2, - ) - - try: - unregister = getattr(self, "_unregister_") - except AttributeError: - pass - else: - unregister() - - if callback is not None: - unregister = self._backend.register_detection_callback(callback) - setattr(self, "_unregister_", unregister) - async def start(self) -> None: """Start scanning for devices""" await self._backend.start() @@ -206,28 +156,9 @@ async def stop(self) -> None: """Stop scanning for devices""" await self._backend.stop() - def set_scanning_filter(self, **kwargs) -> None: - """ - Set scanning filter for the BleakScanner. - - .. deprecated:: 0.17.0 - This method will be removed in a future version of Bleak. Pass - arguments directly to the :class:`BleakScanner` constructor instead. - - Args: - **kwargs: The filter details. - - """ - warn( - "This method will be removed in a future version of Bleak. Use BleakScanner constructor args instead.", - FutureWarning, - stacklevel=2, - ) - self._backend.set_scanning_filter(**kwargs) - async def advertisement_data( self, - ) -> AsyncGenerator[Tuple[BLEDevice, AdvertisementData], None]: + ) -> AsyncGenerator[tuple[BLEDevice, AdvertisementData], None]: """ Yields devices and associated advertising data packets as they are discovered. @@ -239,7 +170,7 @@ async def advertisement_data( .. versionadded:: 0.21 """ - devices = asyncio.Queue() + devices = asyncio.Queue[tuple[BLEDevice, AdvertisementData]]() unregister_callback = self._backend.register_detection_callback( lambda bd, ad: devices.put_nowait((bd, ad)) @@ -256,7 +187,7 @@ class ExtraArgs(TypedDict, total=False): other convenience methods. """ - service_uuids: List[str] + service_uuids: list[str] """ Optional list of service UUIDs to filter on. Only advertisements containing this advertising data will be received. Required on @@ -276,7 +207,7 @@ class ExtraArgs(TypedDict, total=False): """ Dictionary of arguments specific to the CoreBluetooth backend. """ - backend: Type[BaseBleakScanner] + backend: type[BaseBleakScanner] """ Used to override the automatically selected backend (i.e. for a custom backend). @@ -285,18 +216,30 @@ class ExtraArgs(TypedDict, total=False): @overload @classmethod async def discover( - cls, timeout: float = 5.0, *, return_adv: Literal[False] = False, **kwargs - ) -> List[BLEDevice]: ... + cls, + timeout: float = 5.0, + *, + return_adv: Literal[False] = False, + **kwargs: Unpack[ExtraArgs], + ) -> list[BLEDevice]: ... @overload @classmethod async def discover( - cls, timeout: float = 5.0, *, return_adv: Literal[True], **kwargs - ) -> Dict[str, Tuple[BLEDevice, AdvertisementData]]: ... + cls, + timeout: float = 5.0, + *, + return_adv: Literal[True], + **kwargs: Unpack[ExtraArgs], + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: ... @classmethod async def discover( - cls, timeout=5.0, *, return_adv=False, **kwargs: Unpack[ExtraArgs] + cls, + timeout: float = 5.0, + *, + return_adv: bool = False, + **kwargs: Unpack[ExtraArgs], ): """ Scan continuously for ``timeout`` seconds and return discovered devices. @@ -326,7 +269,7 @@ async def discover( return scanner.discovered_devices @property - def discovered_devices(self) -> List[BLEDevice]: + def discovered_devices(self) -> list[BLEDevice]: """ Gets list of the devices that the scanner has discovered during the scanning. @@ -337,7 +280,7 @@ def discovered_devices(self) -> List[BLEDevice]: @property def discovered_devices_and_advertisement_data( self, - ) -> Dict[str, Tuple[BLEDevice, AdvertisementData]]: + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: """ Gets a map of device address to tuples of devices and the most recently received advertisement data for that device. @@ -349,25 +292,7 @@ def discovered_devices_and_advertisement_data( .. versionadded:: 0.19 """ - return self._backend.seen_devices - - async def get_discovered_devices(self) -> List[BLEDevice]: - """Gets the devices registered by the BleakScanner. - - .. deprecated:: 0.11.0 - This method will be removed in a future version of Bleak. Use the - :attr:`.discovered_devices` property instead. - - Returns: - A list of the devices that the scanner has discovered during the scanning. - - """ - warn( - "This method will be removed in a future version of Bleak. Use the `discovered_devices` property instead.", - FutureWarning, - stacklevel=2, - ) - return self.discovered_devices + return {d[0].address: d for d in self._backend.seen_devices.values()} @classmethod async def find_device_by_address( @@ -447,10 +372,43 @@ async def find_device_by_filter( async for bd, ad in scanner.advertisement_data(): if filterfunc(bd, ad): return bd + assert_never(cast(Never, "advertisement_data() should never stop")) except asyncio.TimeoutError: return None +def _resolve_characteristic( + char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], + services: BleakGATTServiceCollection, +) -> BleakGATTCharacteristic: + + if isinstance(char_specifier, BleakGATTCharacteristic): + return char_specifier + + characteristic = services.get_characteristic(char_specifier) + + if not characteristic: + raise BleakCharacteristicNotFoundError(char_specifier) + + return characteristic + + +def _resolve_descriptor( + desc_specifier: Union[BleakGATTDescriptor, int], + services: BleakGATTServiceCollection, +) -> BleakGATTDescriptor: + + if isinstance(desc_specifier, BleakGATTDescriptor): + return desc_specifier + + characteristic = services.get_descriptor(desc_specifier) + + if not characteristic: + raise BleakError(f"Descriptor with handle {desc_specifier} was not found!") + + return characteristic + + class BleakClient: """The Client interface for connecting to a specific BLE GATT server and communicating with it. @@ -477,6 +435,13 @@ class BleakClient: timeout: Timeout in seconds passed to the implicit ``discover`` call when ``address_or_ble_device`` is not a :class:`BLEDevice`. Defaults to 10.0. + pair: + Attempt to pair with the the device before connecting, if it is not + already paired. This has no effect on macOS since pairing is initiated + automatically when accessing a characteristic that requires authentication. + In rare cases, on other platforms, it might be necessary to pair the + device first in order to be able to even enumerate the services during + the connection process. winrt: Dictionary of WinRT/Windows platform-specific options. backend: @@ -485,6 +450,10 @@ class BleakClient: **kwargs: Additional keyword arguments for backwards compatibility. + .. tip:: If you enable pairing with the ``pair`` argument, you will also + want to extend the timeout to allow enough time for the user to find + and enter the PIN code on the device, if required. + .. warning:: Although example code frequently initializes :class:`BleakClient` with a Bluetooth address for simplicity, it is not recommended to do so for more complex use cases. There are several known issues with providing @@ -504,6 +473,9 @@ class BleakClient: .. versionchanged:: 0.18 No longer is alias for backend type and no longer inherits from :class:`BaseBleakClient`. Added ``backend`` parameter. + + .. versionchanged:: 1.0 + Added ``pair`` parameter. """ def __init__( @@ -513,9 +485,10 @@ def __init__( services: Optional[Iterable[str]] = None, *, timeout: float = 10.0, + pair: bool = False, winrt: WinRTClientArgs = {}, - backend: Optional[Type[BaseBleakClient]] = None, - **kwargs, + backend: Optional[type[BaseBleakClient]] = None, + **kwargs: Any, ) -> None: PlatformBleakClient = ( get_platform_client_backend_type() if backend is None else backend @@ -535,6 +508,7 @@ def __init__( winrt=winrt, **kwargs, ) + self._pair_before_connect = pair # device info @@ -566,64 +540,40 @@ def __repr__(self) -> str: # Async Context managers - async def __aenter__(self) -> BleakClient: + async def __aenter__(self) -> Self: await self.connect() return self async def __aexit__( self, - exc_type: Type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: Optional[type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], ) -> None: await self.disconnect() # Connectivity methods - def set_disconnected_callback( - self, callback: Optional[Callable[[BleakClient], None]], **kwargs - ) -> None: - """Set the disconnect callback. - - .. deprecated:: 0.17.0 - This method will be removed in a future version of Bleak. - Pass the callback to the :class:`BleakClient` constructor instead. - - Args: - callback: callback to be called on disconnection. - - """ - warn( - "This method will be removed future version, pass the callback to the BleakClient constructor instead.", - FutureWarning, - stacklevel=2, - ) - self._backend.set_disconnected_callback( - None if callback is None else functools.partial(callback, self), **kwargs - ) - - async def connect(self, **kwargs) -> bool: + async def connect(self, **kwargs: Any) -> None: """Connect to the specified GATT server. Args: **kwargs: For backwards compatibility - should not be used. - Returns: - Always returns ``True`` for backwards compatibility. - + .. versionchanged:: 1.0 + No longer returns ``True``. Instead, the return type is ``None``. """ - return await self._backend.connect(**kwargs) + await self._backend.connect(self._pair_before_connect, **kwargs) - async def disconnect(self) -> bool: + async def disconnect(self) -> None: """Disconnect from the specified GATT server. - Returns: - Always returns ``True`` for backwards compatibility. - + .. versionchanged:: 1.0 + No longer returns ``True``. Instead, the return type is ``None``. """ - return await self._backend.disconnect() + await self._backend.disconnect() - async def pair(self, *args, **kwargs) -> bool: + async def pair(self, *args: Any, **kwargs: Any) -> None: """ Pair with the specified GATT server. @@ -632,13 +582,12 @@ async def pair(self, *args, **kwargs) -> bool: that a characteristic that requires authentication is read or written. This method may have backend-specific additional keyword arguments. - Returns: - Always returns ``True`` for backwards compatibility. - + .. versionchanged:: 1.0 + No longer returns ``True``. Instead, the return type is ``None``. """ - return await self._backend.pair(*args, **kwargs) + await self._backend.pair(*args, **kwargs) - async def unpair(self) -> bool: + async def unpair(self) -> None: """ Unpair from the specified GATT server. @@ -647,10 +596,10 @@ async def unpair(self) -> bool: This method is only available on Windows and Linux and will raise an exception on other platforms. - Returns: - Always returns ``True`` for backwards compatibility. + .. versionchanged:: 1.0 + No longer returns ``True``. Instead, the return type is ``None``. """ - return await self._backend.unpair() + await self._backend.unpair() @property def is_connected(self) -> bool: @@ -665,24 +614,6 @@ def is_connected(self) -> bool: # GATT services methods - async def get_services(self, **kwargs) -> BleakGATTServiceCollection: - """Get all services registered for this GATT server. - - .. deprecated:: 0.17.0 - This method will be removed in a future version of Bleak. - Use the :attr:`services` property instead. - - Returns: - A :class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. - - """ - warn( - "This method will be removed future version, use the services property instead.", - FutureWarning, - stacklevel=2, - ) - return await self._backend.get_services(**kwargs) - @property def services(self) -> BleakGATTServiceCollection: """ @@ -703,7 +634,7 @@ def services(self) -> BleakGATTServiceCollection: async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - **kwargs, + **kwargs: Any, ) -> bytearray: """ Perform read operation on the specified GATT characteristic. @@ -717,14 +648,19 @@ async def read_gatt_char( Returns: The read data. + Raises: + BleakGattCharacteristicNotFoundError: if a characteristic with the + handle or UUID specified by ``char_specifier`` could not be found. + backend-specific exceptions: if the read operation failed. """ - return await self._backend.read_gatt_char(char_specifier, **kwargs) + characteristic = _resolve_characteristic(char_specifier, self.services) + return await self._backend.read_gatt_char(characteristic, **kwargs) async def write_gatt_char( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], data: Buffer, - response: bool = None, + response: Optional[bool] = None, ) -> None: r""" Perform a write operation on the specified GATT characteristic. @@ -738,10 +674,6 @@ async def write_gatt_char( Consult the device's documentation or inspect the properties of the characteristic to find out which kind of writes are supported. - .. tip:: Explicit is better than implicit. Best practice is to always - include an explicit ``response=True`` or ``response=False`` - when calling this method. - Args: char_specifier: The characteristic to write to, specified by either integer @@ -758,8 +690,17 @@ async def write_gatt_char( response: If ``True``, a write-with-response operation will be used. If ``False``, a write-without-response operation will be used. - If omitted or ``None``, the "best" operation will be used - based on the reported properties of the characteristic. + Omitting the argument is deprecated and may raise a warning. + If this arg is omitted, the default behavior is to check the + characteristic properties to see if the "write" property is + present. If it is, a write-with-response operation will be + used. Note: some devices may incorrectly report or omit the + property, which is why an explicit argument is encouraged. + + Raises: + BleakGattCharacteristicNotFoundError: if a characteristic with the + handle or UUID specified by ``char_specifier`` could not be found. + backend-specific exceptions: if the write operation failed. .. versionchanged:: 0.21 The default behavior when ``response=`` is omitted was changed. @@ -770,17 +711,13 @@ async def write_gatt_char( ... await client.write_gatt_char(MY_CHAR_UUID, b"\x00\x01\x02\x03", response=True) """ - if isinstance(char_specifier, BleakGATTCharacteristic): - characteristic = char_specifier - else: - characteristic = self.services.get_characteristic(char_specifier) - - if not characteristic: - raise BleakCharacteristicNotFoundError(char_specifier) + characteristic = _resolve_characteristic(char_specifier, self.services) if response is None: - # if not specified, prefer write-with-response over write-without- + # If not specified, prefer write-with-response over write-without- # response if it is available since it is the more reliable write. + # This assumes that the peripheral correctly reports the + # characteristic properties, so doesn't work in some cases. response = "write" in characteristic.properties await self._backend.write_gatt_char(characteristic, data, response) @@ -791,7 +728,9 @@ async def start_notify( callback: Callable[ [BleakGATTCharacteristic, bytearray], Union[None, Awaitable[None]] ], - **kwargs, + *, + cb: CBStartNotifyArgs = {}, + **kwargs: Any, ) -> None: """ Activate notifications/indications on a characteristic. @@ -814,22 +753,24 @@ def callback(sender: BleakGATTCharacteristic, data: bytearray): callback: The function to be called on notification. Can be regular function or async function. + cb: + CoreBluetooth specific arguments. + Raises: + BleakGattCharacteristicNotFoundError: if a characteristic with the + handle or UUID specified by ``char_specifier`` could not be found. + backend-specific exceptions: if the start notification operation failed. .. versionchanged:: 0.18 The first argument of the callback is now a :class:`BleakGATTCharacteristic` instead of an ``int``. + .. versionchanged:: 1.0 + Added the ``cb`` parameter. """ if not self.is_connected: raise BleakError("Not connected") - if not isinstance(char_specifier, BleakGATTCharacteristic): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - - if not characteristic: - raise BleakCharacteristicNotFoundError(char_specifier) + characteristic = _resolve_characteristic(char_specifier, self.services) if inspect.iscoroutinefunction(callback): @@ -841,7 +782,9 @@ def wrapped_callback(data: bytearray) -> None: else: wrapped_callback = functools.partial(callback, characteristic) - await self._backend.start_notify(characteristic, wrapped_callback, **kwargs) + await self._backend.start_notify( + characteristic, wrapped_callback, cb=cb, **kwargs + ) async def stop_notify( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID] @@ -855,52 +798,64 @@ async def stop_notify( specified by either integer handle, UUID or directly by the BleakGATTCharacteristic object representing it. + Raises: + BleakGattCharacteristicNotFoundError: if a characteristic with the + handle or UUID specified by ``char_specifier`` could not be found. + backend-specific exceptions: if the stop notification operation failed. + .. tip:: Notifications are stopped automatically on disconnect, so this method does not need to be called unless notifications need to be stopped some time before the device disconnects. """ - await self._backend.stop_notify(char_specifier) + characteristic = _resolve_characteristic(char_specifier, self.services) + await self._backend.stop_notify(characteristic) - async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: + async def read_gatt_descriptor( + self, + desc_specifier: Union[BleakGATTDescriptor, int], + **kwargs: Any, + ) -> bytearray: """ Perform read operation on the specified GATT descriptor. Args: - handle: The handle of the descriptor to read from. + desc_specifier: + The descriptor to read from, specified by either integer handle + or directly by the BleakGATTDescriptor object representing it. + + Raises: + BleakError: if the descriptor could not be found. + backend-specific exceptions: if the read operation failed. Returns: The read data. """ - return await self._backend.read_gatt_descriptor(handle, **kwargs) + descriptor = _resolve_descriptor(desc_specifier, self.services) + return await self._backend.read_gatt_descriptor(descriptor, **kwargs) - async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: + async def write_gatt_descriptor( + self, + desc_specifier: Union[BleakGATTDescriptor, int], + data: Buffer, + ) -> None: """ Perform a write operation on the specified GATT descriptor. Args: - handle: - The handle of the descriptor to read from. + desc_specifier: + The descriptor to write to, specified by either integer handle + directly by the BleakGATTDescriptor object representing it. data: The data to send. - """ - await self._backend.write_gatt_descriptor(handle, data) - + Raises: + BleakError: if the descriptor could not be found. + backend-specific exceptions: if the read operation failed. -# for backward compatibility -def discover(*args, **kwargs): - """ - .. deprecated:: 0.17.0 - This method will be removed in a future version of Bleak. - Use :meth:`BleakScanner.discover` instead. - """ - warn( - "The discover function will removed in a future version, use BleakScanner.discover instead.", - FutureWarning, - stacklevel=2, - ) - return BleakScanner.discover(*args, **kwargs) + """ + descriptor = _resolve_descriptor(desc_specifier, self.services) + await self._backend.write_gatt_descriptor(descriptor, data) def cli() -> None: @@ -915,7 +870,9 @@ def cli() -> None: ) args = parser.parse_args() - out = asyncio.run(discover(adapter=args.adapter, timeout=float(args.timeout))) + out = asyncio.run( + BleakScanner.discover(adapter=args.adapter, timeout=float(args.timeout)) + ) for o in out: print(str(o)) diff --git a/bleak/args/__init__.py b/bleak/args/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bleak/args/bluez.py b/bleak/args/bluez.py new file mode 100644 index 000000000..af5d3f7cd --- /dev/null +++ b/bleak/args/bluez.py @@ -0,0 +1,99 @@ +""" +----------------------- +BlueZ backend arguments +----------------------- +""" + +from typing import NamedTuple, TypedDict, Union + +from bleak.assigned_numbers import AdvertisementDataType + + +class BlueZDiscoveryFilters(TypedDict, total=False): + """ + Dictionary of arguments for the ``org.bluez.Adapter1.SetDiscoveryFilter`` + D-Bus method. + + https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst#void-setdiscoveryfilterdict-filter + """ + + UUIDs: list[str] + """ + Filter by service UUIDs, empty means match _any_ UUID. + + Normally, the ``service_uuids`` argument of :class:`bleak.BleakScanner` + is used instead. + """ + RSSI: int + """ + RSSI threshold value. + """ + Pathloss: int + """ + Pathloss threshold value. + """ + Transport: str + """ + Transport parameter determines the type of scan. + + This should not be used since it is required to be set to ``"le"``. + """ + DuplicateData: bool + """ + Disables duplicate detection of advertisement data. + + This does not affect the ``Filter Duplicates`` parameter of the ``LE Set Scan Enable`` + HCI command to the Bluetooth adapter! + + Although the default value for BlueZ is ``True``, Bleak sets this to ``False`` by default. + """ + Discoverable: bool + """ + Make adapter discoverable while discovering, + if the adapter is already discoverable setting + this filter won't do anything. + """ + Pattern: str + """ + Discover devices where the pattern matches + either the prefix of the address or + device name which is convenient way to limited + the number of device objects created during a + discovery. + """ + + +class OrPattern(NamedTuple): + """ + BlueZ advertisement monitor or-pattern. + + https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitor.rst#arrayuint8-uint8-arraybyte-patterns-read-only-optional + """ + + start_position: int + ad_data_type: AdvertisementDataType + content_of_pattern: bytes + + +# Windows has a similar structure, so we allow generic tuple for cross-platform compatibility +OrPatternLike = Union[OrPattern, tuple[int, AdvertisementDataType, bytes]] + + +class BlueZScannerArgs(TypedDict, total=False): + """ + :class:`BleakScanner` args that are specific to the BlueZ backend. + """ + + filters: BlueZDiscoveryFilters + """ + Filters to pass to the adapter SetDiscoveryFilter D-Bus method. + + Only used for active scanning. + """ + + or_patterns: list[OrPatternLike] + """ + Or patterns to pass to the AdvertisementMonitor1 D-Bus interface. + + Only used for passive scanning. + """ diff --git a/bleak/args/corebluetooth.py b/bleak/args/corebluetooth.py new file mode 100644 index 000000000..4850a9574 --- /dev/null +++ b/bleak/args/corebluetooth.py @@ -0,0 +1,41 @@ +""" +------------------------------- +CoreBluetooth backend arguments +------------------------------- +""" + +from collections.abc import Callable +from typing import Optional, TypedDict + + +class CBScannerArgs(TypedDict, total=False): + """ + Platform-specific :class:`BleakScanner` args for the CoreBluetooth backend. + """ + + use_bdaddr: bool + """ + If true, use Bluetooth address instead of UUID. + + .. warning:: This uses an undocumented IOBluetooth API to get the Bluetooth + address and may break in the future macOS releases. `It is known to not + work on macOS 10.15 `_. + """ + + +NotificationDiscriminator = Callable[[bytes], bool] + + +class CBStartNotifyArgs(TypedDict, total=False): + """CoreBluetooth backend-specific dictionary of arguments for the + :meth:`bleak.BleakClient.start_notify` method. + """ + + notification_discriminator: Optional[NotificationDiscriminator] + """ + A function that takes a single argument of a characteristic value + and returns ``True`` if the value is from a notification or + ``False`` if the value is from a read response. + + .. seealso:: :ref:`cb-notification-discriminator` for more info. + """ diff --git a/bleak/args/winrt.py b/bleak/args/winrt.py new file mode 100644 index 000000000..856d9f0af --- /dev/null +++ b/bleak/args/winrt.py @@ -0,0 +1,32 @@ +""" +----------------------- +WinRT backend arguments +----------------------- +""" + +from typing import Literal, TypedDict + + +class WinRTClientArgs(TypedDict, total=False): + """ + Windows-specific arguments for :class:`BleakClient`. + """ + + address_type: Literal["public", "random"] + """ + Can either be ``"public"`` or ``"random"``, depending on the required address + type needed to connect to your device. + """ + + use_cached_services: bool + """ + ``True`` allows Windows to fetch the services, characteristics and descriptors + from the Windows cache instead of reading them from the device. Can be very + much faster for known, unchanging devices, but not recommended for DIY peripherals + where the GATT layout can change between connections. + + ``False`` will force the attribute database to be read from the remote device + instead of using the OS cache. + + If omitted, the OS Bluetooth stack will do what it thinks is best. + """ diff --git a/bleak/assigned_numbers.py b/bleak/assigned_numbers.py index d52220fb3..df1bf6bb5 100644 --- a/bleak/assigned_numbers.py +++ b/bleak/assigned_numbers.py @@ -8,6 +8,7 @@ """ from enum import IntEnum +from typing import Literal class AdvertisementDataType(IntEnum): @@ -36,3 +37,57 @@ class AdvertisementDataType(IntEnum): SERVICE_DATA_UUID128 = 0x21 MANUFACTURER_SPECIFIC_DATA = 0xFF + + +# NOTE: these must match BlueZ name mapping +CharacteristicPropertyName = Literal[ + "broadcast", + "read", + "write-without-response", + "write", + "notify", + "indicate", + "authenticated-signed-writes", + "extended-properties", + "reliable-write", + "writable-auxiliaries", + "encrypt-read", + "encrypt-write", + # "encrypt-notify" and "encrypt-indicate" are server-only + "encrypt-authenticated-read", + "encrypt-authenticated-write", + # "encrypt-authenticated-notify", "encrypt-authenticated-indicate", + # "secure-read", "secure-write", "secure-notify", "secure-indicate" + # are server-only + "authorize", +] + +CHARACTERISTIC_PROPERTIES: dict[int, CharacteristicPropertyName] = { + 0x1: "broadcast", + 0x2: "read", + 0x4: "write-without-response", + 0x8: "write", + 0x10: "notify", + 0x20: "indicate", + 0x40: "authenticated-signed-writes", + 0x80: "extended-properties", + 0x100: "reliable-write", + 0x200: "writable-auxiliaries", +} + + +def gatt_char_props_to_strs( + props: int, +) -> frozenset[CharacteristicPropertyName]: + """ + Convert a GATT characteristic properties bitmask to a set of strings. + + Args: + props: The GATT characteristic properties bitmask. + + Returns: + A set of strings representing the GATT characteristic properties. + """ + return frozenset( + CHARACTERISTIC_PROPERTIES[i] for i in (1 << n for n in range(16)) if props & i + ) diff --git a/bleak/backends/__init__.py b/bleak/backends/__init__.py index 007bc018c..89f934b65 100644 --- a/bleak/backends/__init__.py +++ b/bleak/backends/__init__.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- +# Created on 2017-11-19 by hbldh """ __init__.py - -Created on 2017-11-19 by hbldh - """ diff --git a/bleak/backends/bluezdbus/advertisement_monitor.py b/bleak/backends/bluezdbus/advertisement_monitor.py index 922ffab4f..a2f8472ab 100644 --- a/bleak/backends/bluezdbus/advertisement_monitor.py +++ b/bleak/backends/bluezdbus/advertisement_monitor.py @@ -6,31 +6,42 @@ monitor api `. """ -import logging -from typing import Iterable, NamedTuple, Tuple, Union, no_type_check +import sys +from typing import TYPE_CHECKING -from dbus_fast.service import PropertyAccess, ServiceInterface, dbus_property, method +if TYPE_CHECKING: + if sys.platform != "linux": + assert False, "This backend is only available on Linux" -from ...assigned_numbers import AdvertisementDataType -from . import defs +import logging +from collections.abc import Iterable +from typing import Any, no_type_check +from warnings import warn -logger = logging.getLogger(__name__) +from dbus_fast import PropertyAccess +from dbus_fast.service import ServiceInterface, dbus_property, method +from bleak.args.bluez import OrPattern as _OrPattern +from bleak.args.bluez import OrPatternLike as _OrPatternLike +from bleak.backends.bluezdbus import defs -class OrPattern(NamedTuple): - """ - BlueZ advertisement monitor or-pattern. - - https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitor.rst#arrayuint8-uint8-arraybyte-patterns-read-only-optional - """ +logger = logging.getLogger(__name__) - start_position: int - ad_data_type: AdvertisementDataType - content_of_pattern: bytes +_DEPRECATED: dict[str, Any] = { + "OrPattern": _OrPattern, + "OrPatternLike": _OrPatternLike, +} -# Windows has a similar structure, so we allow generic tuple for cross-platform compatibility -OrPatternLike = Union[OrPattern, Tuple[int, AdvertisementDataType, bytes]] +def __getattr__(name: str): + if value := _DEPRECATED.get(name): + warn( + f"importing {name} from bleak.backends.bluezdbus.advertisement_monitor is deprecated, use bleak.args.bluez instead", + DeprecationWarning, + stacklevel=2, + ) + return value + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") class AdvertisementMonitor(ServiceInterface): @@ -49,7 +60,7 @@ class AdvertisementMonitor(ServiceInterface): def __init__( self, - or_patterns: Iterable[OrPatternLike], + or_patterns: Iterable[_OrPatternLike], ): """ Args: diff --git a/bleak/backends/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py deleted file mode 100644 index ab707827a..000000000 --- a/bleak/backends/bluezdbus/characteristic.py +++ /dev/null @@ -1,107 +0,0 @@ -from typing import Callable, List, Union -from uuid import UUID - -from ..characteristic import BleakGATTCharacteristic -from ..descriptor import BleakGATTDescriptor -from .defs import GattCharacteristic1 -from .utils import extract_service_handle_from_path - -_GattCharacteristicsFlagsEnum = { - 0x0001: "broadcast", - 0x0002: "read", - 0x0004: "write-without-response", - 0x0008: "write", - 0x0010: "notify", - 0x0020: "indicate", - 0x0040: "authenticated-signed-writes", - 0x0080: "extended-properties", - 0x0100: "reliable-write", - 0x0200: "writable-auxiliaries", - # "encrypt-read" - # "encrypt-write" - # "encrypt-authenticated-read" - # "encrypt-authenticated-write" - # "secure-read" #(Server only) - # "secure-write" #(Server only) - # "authorize" -} - - -class BleakGATTCharacteristicBlueZDBus(BleakGATTCharacteristic): - """GATT Characteristic implementation for the BlueZ DBus backend""" - - def __init__( - self, - obj: GattCharacteristic1, - object_path: str, - service_uuid: str, - service_handle: int, - max_write_without_response_size: Callable[[], int], - ): - super(BleakGATTCharacteristicBlueZDBus, self).__init__( - obj, max_write_without_response_size - ) - self.__descriptors = [] - self.__path = object_path - self.__service_uuid = service_uuid - self.__service_handle = service_handle - self._handle = extract_service_handle_from_path(object_path) - - @property - def service_uuid(self) -> str: - """The uuid of the Service containing this characteristic""" - return self.__service_uuid - - @property - def service_handle(self) -> int: - """The handle of the Service containing this characteristic""" - return self.__service_handle - - @property - def handle(self) -> int: - """The handle of this characteristic""" - return self._handle - - @property - def uuid(self) -> str: - """The uuid of this characteristic""" - return self.obj.get("UUID") - - @property - def properties(self) -> List[str]: - """Properties of this characteristic - - Returns the characteristics `Flags` present in the DBus API. - """ - return self.obj["Flags"] - - @property - def descriptors(self) -> List[BleakGATTDescriptor]: - """List of descriptors for this service""" - return self.__descriptors - - def get_descriptor( - self, specifier: Union[int, str, UUID] - ) -> Union[BleakGATTDescriptor, None]: - """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" - try: - if isinstance(specifier, int): - return next(filter(lambda x: x.handle == specifier, self.descriptors)) - else: - return next( - filter(lambda x: x.uuid == str(specifier), self.descriptors) - ) - except StopIteration: - return None - - def add_descriptor(self, descriptor: BleakGATTDescriptor) -> None: - """Add a :py:class:`~BleakGATTDescriptor` to the characteristic. - - Should not be used by end user, but rather by `bleak` itself. - """ - self.__descriptors.append(descriptor) - - @property - def path(self) -> str: - """The DBus path. Mostly needed by `bleak`, not by end user""" - return self.__path diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index ec225584d..b3c25b1d9 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -2,18 +2,26 @@ """ BLE Client for BlueZ on Linux """ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "linux": + assert False, "This backend is only available on Linux" + import asyncio import logging import os -import sys import warnings -from typing import Callable, Dict, Optional, Set, Union, cast -from uuid import UUID +from collections.abc import Callable +from contextlib import AsyncExitStack +from typing import Any, Optional, Union if sys.version_info < (3, 12): - from typing_extensions import Buffer + from typing_extensions import Buffer, override else: from collections.abc import Buffer + from typing import override if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout @@ -25,28 +33,23 @@ from dbus_fast.message import Message from dbus_fast.signature import Variant -from ... import BleakScanner -from ...exc import ( - BleakCharacteristicNotFoundError, - BleakDBusError, - BleakDeviceNotFoundError, - BleakError, -) -from ..characteristic import BleakGATTCharacteristic -from ..client import BaseBleakClient, NotifyCallback -from ..device import BLEDevice -from ..service import BleakGATTServiceCollection -from . import defs -from .characteristic import BleakGATTCharacteristicBlueZDBus -from .manager import get_global_bluez_manager -from .scanner import BleakScannerBlueZDBus -from .utils import assert_reply, get_dbus_authenticator -from .version import BlueZFeatures +from bleak import BleakScanner +from bleak.backends.bluezdbus import defs +from bleak.backends.bluezdbus.manager import get_global_bluez_manager +from bleak.backends.bluezdbus.scanner import BleakScannerBlueZDBus +from bleak.backends.bluezdbus.utils import assert_reply, get_dbus_authenticator +from bleak.backends.bluezdbus.version import BlueZFeatures +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, NotifyCallback +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.backends.device import BLEDevice +from bleak.backends.service import BleakGATTServiceCollection +from bleak.exc import BleakDBusError, BleakDeviceNotFoundError, BleakError logger = logging.getLogger(__name__) # prevent tasks from being garbage collected -_background_tasks: Set[asyncio.Task] = set() +_background_tasks = set[asyncio.Task[None]]() class BleakClientBlueZDBus(BaseBleakClient): @@ -69,8 +72,8 @@ class BleakClientBlueZDBus(BaseBleakClient): def __init__( self, address_or_ble_device: Union[BLEDevice, str], - services: Optional[Set[str]] = None, - **kwargs, + services: Optional[set[str]] = None, + **kwargs: Any, ): super(BleakClientBlueZDBus, self).__init__(address_or_ble_device, **kwargs) # kwarg "device" is for backwards compatibility @@ -89,7 +92,7 @@ def __init__( # D-Bus message bus self._bus: Optional[MessageBus] = None # tracks device watcher subscription - self._remove_device_watcher: Optional[Callable] = None + self._remove_device_watcher: Optional[Callable[[], None]] = None # private backing for is_connected property self._is_connected = False # indicates disconnect request in progress when not None @@ -97,22 +100,22 @@ def __init__( # used to ensure device gets disconnected if event loop crashes self._disconnect_monitor_event: Optional[asyncio.Event] = None # map of characteristic D-Bus object path to notification callback - self._notification_callbacks: Dict[str, NotifyCallback] = {} + self._notification_callbacks: dict[str, NotifyCallback] = {} # used to override mtu_size property self._mtu_size: Optional[int] = None # Connectivity methods - async def connect(self, dangerous_use_bleak_cache: bool = False, **kwargs) -> bool: + @override + async def connect( + self, pair: bool, dangerous_use_bleak_cache: bool = False, **kwargs: Any + ) -> None: """Connect to the specified GATT server. Keyword Args: timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. - Returns: - Boolean representing connection status. - Raises: BleakError: If the device is already connected or if the device could not be found. BleakDBusError: If there was a D-Bus error @@ -126,7 +129,7 @@ async def connect(self, dangerous_use_bleak_cache: bool = False, **kwargs) -> bo if not BlueZFeatures.checked_bluez_version: await BlueZFeatures.check_bluez_version() if not BlueZFeatures.supported_version: - raise BleakError("Bleak requires BlueZ >= 5.43.") + raise BleakError("Bleak requires BlueZ >= 5.55.") # A Discover must have been run before connecting to any devices. # Find the desired device before trying to connect. timeout = kwargs.get("timeout", self._timeout) @@ -150,142 +153,66 @@ async def connect(self, dangerous_use_bleak_cache: bool = False, **kwargs) -> bo async with async_timeout(timeout): while True: - # Each BLE connection session needs a new D-Bus connection to avoid a - # BlueZ quirk where notifications are automatically enabled on reconnect. - self._bus = await MessageBus( - bus_type=BusType.SYSTEM, - negotiate_unix_fd=True, - auth=get_dbus_authenticator(), - ).connect() - - def on_connected_changed(connected: bool) -> None: - if not connected: - logger.debug("Device disconnected (%s)", self._device_path) - - self._is_connected = False - - if self._disconnect_monitor_event: - self._disconnect_monitor_event.set() - self._disconnect_monitor_event = None - - self._cleanup_all() - if self._disconnected_callback is not None: - self._disconnected_callback() - disconnecting_event = self._disconnecting_event - if disconnecting_event: - disconnecting_event.set() - - def on_value_changed(char_path: str, value: bytes) -> None: - callback = self._notification_callbacks.get(char_path) - - if callback: - callback(bytearray(value)) - - watcher = manager.add_device_watcher( - self._device_path, on_connected_changed, on_value_changed - ) - self._remove_device_watcher = lambda: manager.remove_device_watcher( - watcher - ) - - self._disconnect_monitor_event = local_disconnect_monitor_event = ( - asyncio.Event() - ) - - try: - try: - # - # The BlueZ backend does not disconnect devices when the - # application closes or crashes. This can cause problems - # when trying to reconnect to the same device. To work - # around this, we check if the device is already connected. - # - # For additional details see https://github.com/bluez/bluez/issues/89 - # - if manager.is_connected(self._device_path): - logger.debug( - 'skipping calling "Connect" since %s is already connected', - self._device_path, - ) - else: - logger.debug( - "Connecting to BlueZ path %s", self._device_path - ) - reply = await self._bus.call( - Message( - destination=defs.BLUEZ_SERVICE, - interface=defs.DEVICE_INTERFACE, - path=self._device_path, - member="Connect", - ) - ) - - assert reply is not None - - if reply.message_type == MessageType.ERROR: - # This error is often caused by RF interference - # from other Bluetooth or Wi-Fi devices. In many - # cases, retrying will connect successfully. - # Note: this error was added in BlueZ 6.62. - if ( - reply.error_name == "org.bluez.Error.Failed" - and reply.body - and reply.body[0] == "le-connection-abort-by-local" - ): - logger.debug( - "retry due to le-connection-abort-by-local" - ) - - # When this error occurs, BlueZ actually - # connected so we get "Connected" property changes - # that we need to wait for before attempting - # to connect again. - await local_disconnect_monitor_event.wait() - - # Jump way back to the `while True:`` to retry. - continue - - if reply.error_name == ErrorType.UNKNOWN_OBJECT.value: - raise BleakDeviceNotFoundError( - self.address, - f"Device with address {self.address} was not found. It may have been removed from BlueZ when scanning stopped.", - ) - - assert_reply(reply) + async with AsyncExitStack() as stack: + # Each BLE connection session needs a new D-Bus connection to avoid a + # BlueZ quirk where notifications are automatically enabled on reconnect. + self._bus = await MessageBus( + bus_type=BusType.SYSTEM, + negotiate_unix_fd=True, + auth=get_dbus_authenticator(), + ).connect() + + stack.callback(self._cleanup_all) + + def on_connected_changed(connected: bool) -> None: + if not connected: + logger.debug("Device disconnected (%s)", self._device_path) + + self._is_connected = False + + if self._disconnect_monitor_event: + self._disconnect_monitor_event.set() + self._disconnect_monitor_event = None + + self._cleanup_all() + if self._disconnected_callback is not None: + self._disconnected_callback() + disconnecting_event = self._disconnecting_event + if disconnecting_event: + disconnecting_event.set() + + def on_value_changed(char_path: str, value: bytes) -> None: + callback = self._notification_callbacks.get(char_path) + + if callback: + callback(bytearray(value)) + + watcher = manager.add_device_watcher( + self._device_path, on_connected_changed, on_value_changed + ) + self._remove_device_watcher = lambda: manager.remove_device_watcher( + watcher + ) - self._is_connected = True + self._disconnect_monitor_event = local_disconnect_monitor_event = ( + asyncio.Event() + ) - # Create a task that runs until the device is disconnected. - task = asyncio.create_task( - self._disconnect_monitor( - self._bus, - self._device_path, - local_disconnect_monitor_event, - ) - ) - _background_tasks.add(task) - task.add_done_callback(_background_tasks.discard) - - # - # We will try to use the cache if it exists and `dangerous_use_bleak_cache` - # is True. - # - await self.get_services( - dangerous_use_bleak_cache=dangerous_use_bleak_cache - ) + # this effectively cancels the disconnect monitor in case the event + # was not triggered by a D-Bus callback + stack.callback(local_disconnect_monitor_event.set) - return True - except BaseException: + async def disconnect_device() -> None: # Calling Disconnect cancels any pending connect request. Also, - # if connection was successful but get_services() raises (e.g. - # because task was cancelled), the we still need to disconnect + # if connection was successful but _get_services() raises (e.g. + # because task was cancelled), then we still need to disconnect # before passing on the exception. if self._bus: # If disconnected callback already fired, this will be a no-op # since self._bus will be None and the _cleanup_all call will # have already disconnected. try: - reply = await self._bus.call( + disconnect_reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, interface=defs.DEVICE_INTERFACE, @@ -293,8 +220,9 @@ def on_value_changed(char_path: str, value: bytes) -> None: member="Disconnect", ) ) + assert disconnect_reply try: - assert_reply(reply) + assert_reply(disconnect_reply) except BleakDBusError as e: # if the object no longer exists, then we know we # are disconnected for sure, so don't need to log a @@ -306,13 +234,128 @@ def on_value_changed(char_path: str, value: bytes) -> None: f"Failed to cancel connection ({self._device_path}): {e}" ) - raise - except BaseException: - # this effectively cancels the disconnect monitor in case the event - # was not triggered by a D-Bus callback - local_disconnect_monitor_event.set() - self._cleanup_all() - raise + stack.push_async_callback(disconnect_device) + + # The BlueZ backend does not disconnect devices when the + # application closes or crashes. This can cause problems + # when trying to reconnect to the same device. To work + # around this, we check if the device is already connected. + # + # For additional details see https://github.com/bluez/bluez/issues/89 + if manager.is_connected(self._device_path): + logger.debug( + 'skipping calling "Connect" since %s is already connected', + self._device_path, + ) + else: + logger.debug("Connecting to BlueZ path %s", self._device_path) + + # Calling pair will fail if we are already paired, so + # in that case we just call Connect. + if pair and not manager.is_paired(self._device_path): + # Trust means device is authorized + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=self._device_path, + interface=defs.PROPERTIES_INTERFACE, + member="Set", + signature="ssv", + body=[ + defs.DEVICE_INTERFACE, + "Trusted", + Variant("b", True), + ], + ) + ) + assert reply + assert_reply(reply) + + # REVIST: This leaves "Trusted" property set if we + # fail later. Probably not a big deal since we were + # going to trust it anyway. + + # Pairing means device is authenticated + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + interface=defs.DEVICE_INTERFACE, + path=self._device_path, + member="Pair", + ) + ) + + # For resolvable private addresses, the address will + # change after pairing, so we need to update that. + # Hopefully there is no race condition here. D-Bus + # traffic capture shows that Address change happens + # at the same time as Paired property change and + # that PropertiesChanged signal is sent before the + # "Pair" reply is sent. + self.address = manager.get_device_address(self._device_path) + else: + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + interface=defs.DEVICE_INTERFACE, + path=self._device_path, + member="Connect", + ) + ) + assert reply + + if reply.message_type == MessageType.ERROR: + # This error is often caused by RF interference + # from other Bluetooth or Wi-Fi devices. In many + # cases, retrying will connect successfully. + # Note: this error was added in BlueZ 6.62. + if ( + reply.error_name == "org.bluez.Error.Failed" + and reply.body + and reply.body[0] == "le-connection-abort-by-local" + ): + logger.debug( + "retry due to le-connection-abort-by-local" + ) + + # When this error occurs, BlueZ actually + # connected so we get "Connected" property changes + # that we need to wait for before attempting + # to connect again. + await local_disconnect_monitor_event.wait() + + # Jump way back to the `while True:`` to retry. + continue + + if reply.error_name == ErrorType.UNKNOWN_OBJECT.value: + raise BleakDeviceNotFoundError( + self.address, + f"Device with address {self.address} was not found. It may have been removed from BlueZ when scanning stopped.", + ) + + assert_reply(reply) + + self._is_connected = True + + # Create a task that runs until the device is disconnected. + task = asyncio.create_task( + self._disconnect_monitor( + self._bus, + self._device_path, + local_disconnect_monitor_event, + ) + ) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) + + # We will try to use the cache if it exists and `dangerous_use_bleak_cache` + # is True. + await self._get_services( + dangerous_use_bleak_cache=dangerous_use_bleak_cache + ) + + stack.pop_all() + return @staticmethod async def _disconnect_monitor( @@ -378,12 +421,10 @@ def _cleanup_all(self) -> None: # Reset all stored services. self.services = None - async def disconnect(self) -> bool: + @override + async def disconnect(self) -> None: """Disconnect from the specified GATT server. - Returns: - Boolean representing if device is disconnected. - Raises: BleakDBusError: If there was a D-Bus error asyncio.TimeoutError if the device was not disconnected within 10 seconds @@ -395,7 +436,7 @@ async def disconnect(self) -> bool: # we have already called disconnect and closed the D-Bus # connection. logger.debug("already disconnected ({%s})", self._device_path) - return True + return if self._disconnecting_event: # another call to disconnect() is already in progress @@ -414,6 +455,7 @@ async def disconnect(self) -> bool: member="Disconnect", ) ) + assert reply assert_reply(reply) async with async_timeout(10): await self._disconnecting_event.wait() @@ -424,18 +466,14 @@ async def disconnect(self) -> bool: # "PropertiesChanged" signal handler and that it completed successfully assert self._bus is None - return True - - async def pair(self, *args, **kwargs) -> bool: + @override + async def pair(self, *args: Any, **kwargs: Any) -> None: """Pair with the peripheral. You can use ConnectDevice method if you already know the MAC address of the device. Else you need to StartDiscovery, Trust, Pair and Connect in sequence. - - Returns: - Boolean regarding success of pairing. - """ + assert self._bus # See if it is already paired. reply = await self._bus.call( Message( @@ -447,10 +485,11 @@ async def pair(self, *args, **kwargs) -> bool: body=[defs.DEVICE_INTERFACE, "Paired"], ) ) + assert reply assert_reply(reply) if reply.body[0].value: logger.debug("BLE device @ %s is already paired", self.address) - return True + return # Set device as trusted. reply = await self._bus.call( @@ -463,6 +502,7 @@ async def pair(self, *args, **kwargs) -> bool: body=[defs.DEVICE_INTERFACE, "Trusted", Variant("b", True)], ) ) + assert reply assert_reply(reply) logger.debug("Pairing to BLE device @ %s", self.address) @@ -475,29 +515,22 @@ async def pair(self, *args, **kwargs) -> bool: member="Pair", ) ) + assert reply assert_reply(reply) - reply = await self._bus.call( - Message( - destination=defs.BLUEZ_SERVICE, - path=self._device_path, - interface=defs.PROPERTIES_INTERFACE, - member="Get", - signature="ss", - body=[defs.DEVICE_INTERFACE, "Paired"], - ) - ) - assert_reply(reply) - - return reply.body[0].value - - async def unpair(self) -> bool: - """Unpair with the peripheral. - - Returns: - Boolean regarding success of unpairing. + # For resolvable private addresses, the address will + # change after pairing, so we need to update that. + # Hopefully there is no race condition here. D-Bus + # traffic capture shows that Address change happens + # at the same time as Paired property change and + # that PropertiesChanged signal is sent before the + # "Pair" reply is sent. + manager = await get_global_bluez_manager() + self.address = manager.get_device_address(self._device_path) - """ + @override + async def unpair(self) -> None: + """Unpair with the peripheral.""" adapter_path = await self._get_adapter_path() device_path = await self._get_device_path() manager = await get_global_bluez_manager() @@ -518,6 +551,8 @@ async def unpair(self) -> bool: self._device_info = None self._is_connected = False + assert manager._bus + try: reply = await manager._bus.call( Message( @@ -529,6 +564,7 @@ async def unpair(self) -> bool: body=[device_path], ) ) + assert reply assert_reply(reply) except BleakDBusError as e: if e.dbus_error == "org.bluez.Error.DoesNotExist": @@ -537,9 +573,8 @@ async def unpair(self) -> bool: ) from e raise - return True - @property + @override def is_connected(self) -> bool: """Check connection status between this client and the server. @@ -547,9 +582,7 @@ def is_connected(self) -> bool: Boolean representing connection status. """ - return self._DeprecatedIsConnectedReturn( - False if self._bus is None else self._is_connected - ) + return False if self._bus is None else self._is_connected async def _acquire_mtu(self) -> None: """Acquires the MTU for this device by calling the "AcquireWrite" or @@ -561,6 +594,11 @@ async def _acquire_mtu(self) -> None: If a device uses encryption on characteristics, it will need to be bonded first before calling this method. """ + + assert ( + self.services is not None + ), "Services must be acquired before acquiring MTU" + # This will try to get the "best" characteristic for getting the MTU. # We would rather not start notifications if we don't have to. try: @@ -578,16 +616,19 @@ async def _acquire_mtu(self) -> None: if "notify" in c.properties ) + assert self._bus + reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, - path=char.path, + path=char.obj[0], interface=defs.GATT_CHARACTERISTIC_INTERFACE, member=method, signature="a{sv}", body=[{}], ) ) + assert reply assert_reply(reply) # we aren't actually using the write or notify, we just want the MTU @@ -626,6 +667,7 @@ async def _get_device_path(self) -> str: return f"{adapter_path}/dev_{bluez_address}" @property + @override def mtu_size(self) -> int: """Get ATT MTU size for active connection""" if self._mtu_size is None: @@ -638,8 +680,8 @@ def mtu_size(self) -> int: # GATT services methods - async def get_services( - self, dangerous_use_bleak_cache: bool = False, **kwargs + async def _get_services( + self, dangerous_use_bleak_cache: bool = False, **kwargs: Any ) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. @@ -666,17 +708,14 @@ async def get_services( # IO methods + @override async def read_gatt_char( - self, - char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID], - **kwargs, + self, characteristic: BleakGATTCharacteristic, **kwargs: Any ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to read from, - specified by either integer handle, UUID or directly by the - BleakGATTCharacteristicBlueZDBus object representing it. + characteristic (BleakGATTCharacteristic): The characteristic to read from. Returns: (bytearray) The read data. @@ -685,60 +724,13 @@ async def read_gatt_char( if not self.is_connected: raise BleakError("Not connected") - if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - - if not characteristic: - # Special handling for BlueZ >= 5.48, where Battery Service (0000180f-0000-1000-8000-00805f9b34fb:) - # has been moved to interface org.bluez.Battery1 instead of as a regular service. - if ( - str(char_specifier) == "00002a19-0000-1000-8000-00805f9b34fb" - and BlueZFeatures.hides_battery_characteristic - ): - reply = await self._bus.call( - Message( - destination=defs.BLUEZ_SERVICE, - path=self._device_path, - interface=defs.PROPERTIES_INTERFACE, - member="GetAll", - signature="s", - body=[defs.BATTERY_INTERFACE], - ) - ) - assert_reply(reply) - # Simulate regular characteristics read to be consistent over all platforms. - value = bytearray([reply.body[0]["Percentage"].value]) - logger.debug( - "Read Battery Level {0} | {1}: {2}".format( - char_specifier, self._device_path, value - ) - ) - return value - if ( - str(char_specifier) == "00002a00-0000-1000-8000-00805f9b34fb" - and BlueZFeatures.hides_device_name_characteristic - ): - # Simulate regular characteristics read to be consistent over all platforms. - manager = await get_global_bluez_manager() - value = bytearray(manager.get_device_name(self._device_path).encode()) - logger.debug( - "Read Device Name {0} | {1}: {2}".format( - char_specifier, self._device_path, value - ) - ) - return value - - raise BleakCharacteristicNotFoundError(char_specifier) - while True: assert self._bus reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, - path=characteristic.path, + path=characteristic.obj[0], interface=defs.GATT_CHARACTERISTIC_INTERFACE, member="ReadValue", signature="a{sv}", @@ -762,35 +754,33 @@ async def read_gatt_char( logger.debug( "Read Characteristic {0} | {1}: {2}".format( - characteristic.uuid, characteristic.path, value + characteristic.uuid, characteristic.obj[0], value ) ) return value - async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: + @override + async def read_gatt_descriptor( + self, descriptor: BleakGATTDescriptor, **kwargs: Any + ) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: - handle (int): The handle of the descriptor to read from. + descriptor: The descriptor to read from. Returns: - (bytearray) The read data. - + The read data. """ if not self.is_connected: raise BleakError("Not connected") - descriptor = self.services.get_descriptor(handle) - if not descriptor: - raise BleakError("Descriptor with handle {0} was not found!".format(handle)) - while True: assert self._bus reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, - path=descriptor.path, + path=descriptor.obj[0], interface=defs.GATT_DESCRIPTOR_INTERFACE, member="ReadValue", signature="a{sv}", @@ -812,84 +802,58 @@ async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: value = bytearray(reply.body[0]) - logger.debug("Read Descriptor %s | %s: %s", handle, descriptor.path, value) + logger.debug( + "Read Descriptor %s | %s: %s", descriptor.handle, descriptor.obj[0], value + ) return value + @override async def write_gatt_char( - self, - characteristic: BleakGATTCharacteristic, - data: Buffer, - response: bool, + self, characteristic: BleakGATTCharacteristic, data: Buffer, response: bool ) -> None: if not self.is_connected: raise BleakError("Not connected") - # See docstring for details about this handling. - if not response and not BlueZFeatures.can_write_without_response: - raise BleakError("Write without response requires at least BlueZ 5.46") - - if response or not BlueZFeatures.write_without_response_workaround_needed: - while True: - assert self._bus - - reply = await self._bus.call( - Message( - destination=defs.BLUEZ_SERVICE, - path=characteristic.path, - interface=defs.GATT_CHARACTERISTIC_INTERFACE, - member="WriteValue", - signature="aya{sv}", - body=[ - bytes(data), - { - "type": Variant( - "s", "request" if response else "command" - ) - }, - ], - ) - ) - - assert reply - - if reply.error_name == "org.bluez.Error.InProgress": - logger.debug("retrying characteristic WriteValue due to InProgress") - # Avoid calling in a tight loop. There is no dbus signal to - # indicate ready, so unfortunately, we have to poll. - await asyncio.sleep(0.01) - continue + while True: + assert self._bus - assert_reply(reply) - break - else: - # Older versions of BlueZ don't have the "type" option, so we have - # to write the hard way. This isn't the most efficient way of doing - # things, but it works. reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, - path=characteristic.path, + path=characteristic.obj[0], interface=defs.GATT_CHARACTERISTIC_INTERFACE, - member="AcquireWrite", - signature="a{sv}", - body=[{}], + member="WriteValue", + signature="aya{sv}", + body=[ + bytes(data), + {"type": Variant("s", "request" if response else "command")}, + ], ) ) + + assert reply + + if reply.error_name == "org.bluez.Error.InProgress": + logger.debug("retrying characteristic WriteValue due to InProgress") + # Avoid calling in a tight loop. There is no dbus signal to + # indicate ready, so unfortunately, we have to poll. + await asyncio.sleep(0.01) + continue + assert_reply(reply) - fd = reply.unix_fds[0] - try: - os.write(fd, data) - finally: - os.close(fd) + break logger.debug( "Write Characteristic %s | %s: %s", characteristic.uuid, - characteristic.path, + characteristic.obj[0], data, ) - async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: + @override + async def write_gatt_descriptor( + self, descriptor: BleakGATTDescriptor, data: Buffer + ) -> None: """Perform a write operation on the specified GATT descriptor. Args: @@ -900,18 +864,13 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: if not self.is_connected: raise BleakError("Not connected") - descriptor = self.services.get_descriptor(handle) - - if not descriptor: - raise BleakError(f"Descriptor with handle {handle} was not found!") - while True: assert self._bus reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, - path=descriptor.path, + path=descriptor.obj[0], interface=defs.GATT_DESCRIPTOR_INTERFACE, member="WriteValue", signature="aya{sv}", @@ -931,63 +890,57 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: assert_reply(reply) break - logger.debug("Write Descriptor %s | %s: %s", handle, descriptor.path, data) + logger.debug( + "Write Descriptor %s | %s: %s", descriptor.handle, descriptor.obj[0], data + ) + @override async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, - **kwargs, + **kwargs: Any, ) -> None: """ Activate notifications/indications on a characteristic. """ - characteristic = cast(BleakGATTCharacteristicBlueZDBus, characteristic) - - self._notification_callbacks[characteristic.path] = callback + self._notification_callbacks[characteristic.obj[0]] = callback assert self._bus is not None reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, - path=characteristic.path, + path=characteristic.obj[0], interface=defs.GATT_CHARACTERISTIC_INTERFACE, member="StartNotify", ) ) + assert reply assert_reply(reply) - async def stop_notify( - self, - char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID], - ) -> None: + @override + async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None: """Deactivate notification/indication on a specified characteristic. Args: - char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to deactivate - notification/indication on, specified by either integer handle, UUID or - directly by the BleakGATTCharacteristicBlueZDBus object representing it. - + characteristic (BleakGATTCharacteristic): The characteristic to deactivate + notification/indication on. """ if not self.is_connected: raise BleakError("Not connected") - if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - if not characteristic: - raise BleakCharacteristicNotFoundError(char_specifier) + assert self._bus is not None reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, - path=characteristic.path, + path=characteristic.obj[0], interface=defs.GATT_CHARACTERISTIC_INTERFACE, member="StopNotify", ) ) + assert reply assert_reply(reply) - self._notification_callbacks.pop(characteristic.path, None) + self._notification_callbacks.pop(characteristic.obj[0], None) diff --git a/bleak/backends/bluezdbus/defs.py b/bleak/backends/bluezdbus/defs.py index 18ed0c331..6b615fb52 100644 --- a/bleak/backends/bluezdbus/defs.py +++ b/bleak/backends/bluezdbus/defs.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Literal, Tuple, TypedDict +from typing import Literal, TypedDict + +from bleak.assigned_numbers import CharacteristicPropertyName # DBus Interfaces OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager" @@ -37,10 +39,10 @@ class Adapter1(TypedDict): PairableTimeout: int DiscoverableTimeout: int Discovering: int - UUIDs: List[str] + UUIDs: list[str] Modalias: str - Roles: List[str] - ExperimentalFeatures: List[str] + Roles: list[str] + ExperimentalFeatures: list[str] # https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitor.rst @@ -53,23 +55,23 @@ class AdvertisementMonitor1(TypedDict): RSSILowTimeout: int RSSIHighTimeout: int RSSISamplingPeriod: int - Patterns: List[Tuple[int, int, bytes]] + Patterns: list[tuple[int, int, bytes]] # https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitorManager.rst class AdvertisementMonitorManager1(TypedDict): - SupportedMonitorTypes: List[str] - SupportedFeatures: List[str] + SupportedMonitorTypes: list[str] + SupportedFeatures: list[str] # https://github.com/bluez/bluez/blob/master/doc/org.bluez.Battery.rst class Battery1(TypedDict): - SupportedMonitorTypes: List[str] - SupportedFeatures: List[str] + SupportedMonitorTypes: list[str] + SupportedFeatures: list[str] # https://github.com/bluez/bluez/blob/master/doc/org.bluez.Device.rst @@ -82,7 +84,7 @@ class Device1(TypedDict): Icon: str Class: int Appearance: int - UUIDs: List[str] + UUIDs: list[str] Paired: bool Bonded: bool Connected: bool @@ -95,11 +97,11 @@ class Device1(TypedDict): Modalias: str RSSI: int TxPower: int - ManufacturerData: Dict[int, bytes] - ServiceData: Dict[str, bytes] + ManufacturerData: dict[int, bytes] + ServiceData: dict[str, bytes] ServicesResolved: bool AdvertisingFlags: bytes - AdvertisingData: Dict[int, bytes] + AdvertisingData: dict[int, bytes] # https://github.com/bluez/bluez/blob/master/doc/org.bluez.GattService.rst @@ -109,7 +111,7 @@ class GattService1(TypedDict): UUID: str Primary: bool Device: str - Includes: List[str] + Includes: list[str] # Handle is server-only and not available in Bleak @@ -120,29 +122,7 @@ class GattCharacteristic1(TypedDict): WriteAcquired: bool NotifyAcquired: bool Notifying: bool - Flags: List[ - Literal[ - "broadcast", - "read", - "write-without-response", - "write", - "notify", - "indicate", - "authenticated-signed-writes", - "extended-properties", - "reliable-write", - "writable-auxiliaries", - "encrypt-read", - "encrypt-write", - # "encrypt-notify" and "encrypt-indicate" are server-only - "encrypt-authenticated-read", - "encrypt-authenticated-write", - # "encrypt-authenticated-notify", "encrypt-authenticated-indicate", - # "secure-read", "secure-write", "secure-notify", "secure-indicate" - # are server-only - "authorize", - ] - ] + Flags: list[CharacteristicPropertyName] MTU: int # Handle is server-only and not available in Bleak @@ -151,7 +131,7 @@ class GattDescriptor1(TypedDict): UUID: str Characteristic: str Value: bytes - Flags: List[ + Flags: list[ Literal[ "read", "write", diff --git a/bleak/backends/bluezdbus/descriptor.py b/bleak/backends/bluezdbus/descriptor.py deleted file mode 100644 index bf3507990..000000000 --- a/bleak/backends/bluezdbus/descriptor.py +++ /dev/null @@ -1,44 +0,0 @@ -from ..descriptor import BleakGATTDescriptor -from .defs import GattDescriptor1 - - -class BleakGATTDescriptorBlueZDBus(BleakGATTDescriptor): - """GATT Descriptor implementation for BlueZ DBus backend""" - - def __init__( - self, - obj: GattDescriptor1, - object_path: str, - characteristic_uuid: str, - characteristic_handle: int, - ): - super(BleakGATTDescriptorBlueZDBus, self).__init__(obj) - self.__path = object_path - self.__characteristic_uuid = characteristic_uuid - self.__characteristic_handle = characteristic_handle - self.__handle = int(self.path.split("/")[-1].replace("desc", ""), 16) - - @property - def characteristic_handle(self) -> int: - """Handle for the characteristic that this descriptor belongs to""" - return self.__characteristic_handle - - @property - def characteristic_uuid(self) -> str: - """UUID for the characteristic that this descriptor belongs to""" - return self.__characteristic_uuid - - @property - def uuid(self) -> str: - """UUID for this descriptor""" - return self.obj["UUID"] - - @property - def handle(self) -> int: - """Integer handle for this descriptor""" - return self.__handle - - @property - def path(self) -> str: - """The DBus path. Mostly needed by `bleak`, not by end user""" - return self.__path diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index cedbd6420..b42bbc8a9 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -6,42 +6,45 @@ used internally by Bleak. """ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "linux": + assert False, "This backend is only available on Linux" + import asyncio import contextlib import logging import os from collections import defaultdict -from typing import ( - Any, - Callable, - Coroutine, - Dict, - List, - MutableMapping, - NamedTuple, - Optional, - Set, - cast, -) +from collections.abc import Callable, Coroutine, MutableMapping +from typing import Any, NamedTuple, Optional, cast from weakref import WeakKeyDictionary from dbus_fast import BusType, Message, MessageType, Variant, unpack_variants from dbus_fast.aio.message_bus import MessageBus -from ...exc import BleakDBusError, BleakError -from ..service import BleakGATTServiceCollection -from . import defs -from .advertisement_monitor import AdvertisementMonitor, OrPatternLike -from .characteristic import BleakGATTCharacteristicBlueZDBus -from .defs import Device1, GattCharacteristic1, GattDescriptor1, GattService1 -from .descriptor import BleakGATTDescriptorBlueZDBus -from .service import BleakGATTServiceBlueZDBus -from .signals import MatchRules, add_match -from .utils import ( +from bleak.args.bluez import OrPatternLike +from bleak.backends.bluezdbus import defs +from bleak.backends.bluezdbus.advertisement_monitor import AdvertisementMonitor +from bleak.backends.bluezdbus.defs import ( + Device1, + GattCharacteristic1, + GattDescriptor1, + GattService1, +) +from bleak.backends.bluezdbus.signals import MatchRules, add_match +from bleak.backends.bluezdbus.utils import ( assert_reply, device_path_from_characteristic_path, + extract_service_handle_from_path, get_dbus_authenticator, ) +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection +from bleak.exc import BleakDBusError, BleakError logger = logging.getLogger(__name__) @@ -158,33 +161,33 @@ class BlueZManager: Use :func:`bleak.backends.bluezdbus.get_global_bluez_manager` to get the global instance. """ - def __init__(self): + def __init__(self) -> None: self._bus: Optional[MessageBus] = None self._bus_lock = asyncio.Lock() # dict of object path: dict of interface name: dict of property name: property value - self._properties: Dict[str, Dict[str, Dict[str, Any]]] = {} + self._properties: dict[str, dict[str, dict[str, Any]]] = {} # set of available adapters for quick lookup - self._adapters: Set[str] = set() + self._adapters = set[str]() # The BlueZ APIs only maps children to parents, so we need to keep maps # to quickly find the children of a parent D-Bus object. # map of device d-bus object paths to set of service d-bus object paths - self._service_map: Dict[str, Set[str]] = {} + self._service_map: dict[str, set[str]] = {} # map of service d-bus object paths to set of characteristic d-bus object paths - self._characteristic_map: Dict[str, Set[str]] = {} + self._characteristic_map: dict[str, set[str]] = {} # map of characteristic d-bus object paths to set of descriptor d-bus object paths - self._descriptor_map: Dict[str, Set[str]] = {} + self._descriptor_map: dict[str, set[str]] = {} - self._advertisement_callbacks: defaultdict[str, List[AdvertisementCallback]] = ( + self._advertisement_callbacks: defaultdict[str, list[AdvertisementCallback]] = ( defaultdict(list) ) - self._device_removed_callbacks: List[DeviceRemovedCallbackAndState] = [] - self._device_watchers: Dict[str, Set[DeviceWatcher]] = {} - self._condition_callbacks: Dict[str, Set[DeviceConditionCallback]] = {} - self._services_cache: Dict[str, BleakGATTServiceCollection] = {} + self._device_removed_callbacks: list[DeviceRemovedCallbackAndState] = [] + self._device_watchers: dict[str, set[DeviceWatcher]] = {} + self._condition_callbacks: dict[str, set[DeviceConditionCallback]] = {} + self._services_cache: dict[str, BleakGATTServiceCollection] = {} def _check_adapter(self, adapter_path: str) -> None: """ @@ -247,6 +250,7 @@ async def async_init(self) -> None: # Add signal listeners bus.add_message_handler(self._parse_msg) + reply: Optional[Message] rules = MatchRules( interface=defs.OBJECT_MANAGER_INTERFACE, @@ -283,6 +287,7 @@ async def async_init(self) -> None: interface=defs.OBJECT_MANAGER_INTERFACE, ) ) + assert reply assert_reply(reply) # dictionaries are cleared in case AddInterfaces was received first @@ -363,10 +368,10 @@ def get_default_adapter(self) -> str: async def active_scan( self, adapter_path: str, - filters: Dict[str, Variant], + filters: dict[str, Variant], advertisement_callback: AdvertisementCallback, device_removed_callback: DeviceRemovedCallback, - ) -> Callable[[], Coroutine]: + ) -> Callable[[], Coroutine[Any, Any, None]]: """ Configures the advertisement data filters and starts scanning. @@ -385,6 +390,8 @@ async def active_scan( BleakError: if the adapter is not present in BlueZ """ async with self._bus_lock: + assert self._bus + # If the adapter doesn't exist, then the message calls below would # fail with "method not found". This provides a more informative # error message. @@ -409,6 +416,7 @@ async def active_scan( body=[filters], ) ) + assert reply assert_reply(reply) # Start scanning @@ -420,6 +428,7 @@ async def active_scan( member="StartDiscovery", ) ) + assert reply assert_reply(reply) async def stop() -> None: @@ -434,6 +443,8 @@ async def stop() -> None: ) async with self._bus_lock: + assert self._bus + reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, @@ -442,6 +453,7 @@ async def stop() -> None: member="StopDiscovery", ) ) + assert reply try: assert_reply(reply) @@ -460,6 +472,7 @@ async def stop() -> None: body=[{}], ) ) + assert reply assert_reply(reply) return stop @@ -474,10 +487,10 @@ async def stop() -> None: async def passive_scan( self, adapter_path: str, - filters: List[OrPatternLike], + filters: list[OrPatternLike], advertisement_callback: AdvertisementCallback, device_removed_callback: DeviceRemovedCallback, - ) -> Callable[[], Coroutine]: + ) -> Callable[[], Coroutine[Any, Any, None]]: """ Configures the advertisement data filters and starts scanning. @@ -496,6 +509,8 @@ async def passive_scan( BleakError: if the adapter is not present in BlueZ """ async with self._bus_lock: + assert self._bus + # If the adapter doesn't exist, then the message calls below would # fail with "method not found". This provides a more informative # error message. @@ -525,6 +540,7 @@ async def passive_scan( body=[monitor_path], ) ) + assert reply if ( reply.message_type == MessageType.ERROR @@ -552,6 +568,8 @@ async def stop() -> None: ) async with self._bus_lock: + assert self._bus + self._bus.unexport(monitor_path, monitor) reply = await self._bus.call( @@ -564,6 +582,7 @@ async def stop() -> None: body=[monitor_path], ) ) + assert reply assert_reply(reply) return stop @@ -625,7 +644,7 @@ def remove_device_watcher(self, watcher: DeviceWatcher) -> None: del self._device_watchers[device_path] async def get_services( - self, device_path: str, use_cached: bool, requested_services: Optional[Set[str]] + self, device_path: str, use_cached: bool, requested_services: Optional[set[str]] ) -> BleakGATTServiceCollection: """ Builds a new :class:`BleakGATTServiceCollection` from the current state. @@ -665,7 +684,11 @@ async def get_services( self._properties[service_path][defs.GATT_SERVICE_INTERFACE], ) - service = BleakGATTServiceBlueZDBus(service_props, service_path) + service = BleakGATTService( + (service_path, service_props), + extract_service_handle_from_path(service_path), + service_props["UUID"], + ) if ( requested_services is not None @@ -681,14 +704,15 @@ async def get_services( self._properties[char_path][defs.GATT_CHARACTERISTIC_INTERFACE], ) - char = BleakGATTCharacteristicBlueZDBus( - char_props, - char_path, - service.uuid, - service.handle, + char = BleakGATTCharacteristic( + (char_path, char_props), + extract_service_handle_from_path(char_path), + char_props["UUID"], + char_props["Flags"], # "MTU" property was added in BlueZ 5.62, otherwise fall # back to minimum MTU according to Bluetooth spec. lambda: char_props.get("MTU", 23) - 3, + service, ) services.add_characteristic(char) @@ -699,11 +723,11 @@ async def get_services( self._properties[desc_path][defs.GATT_DESCRIPTOR_INTERFACE], ) - desc = BleakGATTDescriptorBlueZDBus( - desc_props, - desc_path, - char.uuid, - char.handle, + desc = BleakGATTDescriptor( + (desc_path, desc_props), + int(desc_path[-4:], 16), + desc_props["UUID"], + char, ) services.add_descriptor(desc) @@ -727,6 +751,21 @@ def get_device_name(self, device_path: str) -> str: """ return self._get_device_property(device_path, defs.DEVICE_INTERFACE, "Name") + def get_device_address(self, device_path: str) -> str: + """ + Gets the value of the "Address" property for a device. + + Args: + device_path: The D-Bus object path of the device. + + Returns: + The current property value. + + Raises: + BleakError: if the device is not present in BlueZ + """ + return self._get_device_property(device_path, defs.DEVICE_INTERFACE, "Address") + def is_connected(self, device_path: str) -> bool: """ Gets the value of the "Connected" property for a device. @@ -742,6 +781,21 @@ def is_connected(self, device_path: str) -> bool: except KeyError: return False + def is_paired(self, device_path: str) -> bool: + """ + Gets the value of the "Paired" property for a device. + + Args: + device_path: The D-Bus object path of the device. + + Returns: + The current property value or ``False`` if the device does not exist in BlueZ. + """ + try: + return self._properties[device_path][defs.DEVICE_INTERFACE]["Paired"] + except KeyError: + return False + async def _wait_for_services_discovery(self, device_path: str) -> None: """ Waits for the device services to be discovered. @@ -879,11 +933,11 @@ def _parse_msg(self, message: Message) -> None: # type hints obj_path: str - interfaces_and_props: Dict[str, Dict[str, Variant]] - interfaces: List[str] + interfaces_and_props: dict[str, dict[str, Variant]] + interfaces: list[str] interface: str - changed: Dict[str, Variant] - invalidated: List[str] + changed: dict[str, Variant] + invalidated: list[str] if message.member == "InterfacesAdded": obj_path, interfaces_and_props = message.body @@ -945,6 +999,13 @@ def _parse_msg(self, message: Message) -> None: if obj_path.startswith(adapter_path): callback(obj_path) elif interface == defs.GATT_SERVICE_INTERFACE: + device_path = obj_path[: obj_path.rfind("/")] + + try: + self._service_map[device_path].remove(obj_path) + except KeyError: + pass + try: del self._characteristic_map[obj_path] except KeyError: @@ -966,7 +1027,7 @@ def _parse_msg(self, message: Message) -> None: assert message_path is not None try: - self_interface = self._properties[message.path][interface] + self_interface = self._properties[message_path][interface] except KeyError: # This can happen during initialization. The "PropertiesChanged" # handler is attached before "GetManagedObjects" is called @@ -1001,10 +1062,10 @@ def _parse_msg(self, message: Message) -> None: # handle device condition watchers callbacks = self._condition_callbacks.get(device_path) if callbacks: - for callback in callbacks: - name = callback.property_name + for item in callbacks: + name = item.property_name if name in changed: - callback.callback(self_interface.get(name)) + item.callback(self_interface.get(name)) # handle device connection change watchers if "Connected" in changed: diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 186e7d42b..1d5c9983b 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -1,91 +1,51 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "linux": + assert False, "This backend is only available on Linux" + import logging -from typing import Callable, Coroutine, Dict, List, Literal, Optional, TypedDict +from collections.abc import Callable, Coroutine +from typing import Any, Literal, Optional from warnings import warn +if sys.version_info < (3, 12): + from typing_extensions import override +else: + from typing import override + from dbus_fast import Variant -from ...exc import BleakError -from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner -from .advertisement_monitor import OrPatternLike -from .defs import Device1 -from .manager import get_global_bluez_manager -from .utils import bdaddr_from_device_path +from bleak.args.bluez import BlueZDiscoveryFilters as _BlueZDiscoveryFilters +from bleak.args.bluez import BlueZScannerArgs as _BlueZScannerArgs +from bleak.backends.bluezdbus.defs import Device1 +from bleak.backends.bluezdbus.manager import get_global_bluez_manager +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BaseBleakScanner, +) +from bleak.exc import BleakError logger = logging.getLogger(__name__) -class BlueZDiscoveryFilters(TypedDict, total=False): - """ - Dictionary of arguments for the ``org.bluez.Adapter1.SetDiscoveryFilter`` - D-Bus method. - - https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst#void-setdiscoveryfilterdict-filter - """ - - UUIDs: List[str] - """ - Filter by service UUIDs, empty means match _any_ UUID. - - Normally, the ``service_uuids`` argument of :class:`bleak.BleakScanner` - is used instead. - """ - RSSI: int - """ - RSSI threshold value. - """ - Pathloss: int - """ - Pathloss threshold value. - """ - Transport: str - """ - Transport parameter determines the type of scan. - - This should not be used since it is required to be set to ``"le"``. - """ - DuplicateData: bool - """ - Disables duplicate detection of advertisement data. +_DEPRECATED: dict[str, Any] = { + "BlueZDiscoveryFilters": _BlueZDiscoveryFilters, + "BlueZScannerArgs": _BlueZScannerArgs, +} - This does not affect the ``Filter Duplicates`` parameter of the ``LE Set Scan Enable`` - HCI command to the Bluetooth adapter! - Although the default value for BlueZ is ``True``, Bleak sets this to ``False`` by default. - """ - Discoverable: bool - """ - Make adapter discoverable while discovering, - if the adapter is already discoverable setting - this filter won't do anything. - """ - Pattern: str - """ - Discover devices where the pattern matches - either the prefix of the address or - device name which is convenient way to limited - the number of device objects created during a - discovery. - """ - - -class BlueZScannerArgs(TypedDict, total=False): - """ - :class:`BleakScanner` args that are specific to the BlueZ backend. - """ - - filters: BlueZDiscoveryFilters - """ - Filters to pass to the adapter SetDiscoveryFilter D-Bus method. - - Only used for active scanning. - """ - - or_patterns: List[OrPatternLike] - """ - Or patterns to pass to the AdvertisementMonitor1 D-Bus interface. - - Only used for passive scanning. - """ +def __getattr__(name: str): + if value := _DEPRECATED.get(name): + warn( + f"importing {name} from bleak.backends.bluezdbus.scanner is deprecated, use bleak.args.bluez instead", + DeprecationWarning, + stacklevel=2, + ) + return value + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") class BleakScannerBlueZDBus(BaseBleakScanner): @@ -114,11 +74,11 @@ class BleakScannerBlueZDBus(BaseBleakScanner): def __init__( self, detection_callback: Optional[AdvertisementDataCallback], - service_uuids: Optional[List[str]], + service_uuids: Optional[list[str]], scanning_mode: Literal["active", "passive"], *, - bluez: BlueZScannerArgs, - **kwargs, + bluez: _BlueZScannerArgs, + **kwargs: Any, ): super(BleakScannerBlueZDBus, self).__init__(detection_callback, service_uuids) @@ -128,11 +88,11 @@ def __init__( self._adapter: Optional[str] = kwargs.get("adapter", kwargs.get("device")) # callback from manager for stopping scanning if it has been started - self._stop: Optional[Callable[[], Coroutine]] = None + self._stop: Optional[Callable[[], Coroutine[Any, Any, None]]] = None # Discovery filters - self._filters: Dict[str, Variant] = {} + self._filters: dict[str, Variant] = {} self._filters["Transport"] = Variant("s", "le") self._filters["DuplicateData"] = Variant("b", False) @@ -140,16 +100,7 @@ def __init__( if self._service_uuids: self._filters["UUIDs"] = Variant("as", self._service_uuids) - filters = kwargs.get("filters") - - if filters is None: - filters = bluez.get("filters") - else: - warn( - "the 'filters' kwarg is deprecated, use 'bluez' kwarg instead", - FutureWarning, - stacklevel=2, - ) + filters = bluez.get("filters") if filters is not None: self.set_scanning_filter(filters=filters) @@ -164,6 +115,7 @@ def __init__( if self._scanning_mode == "passive" and not self._or_patterns: raise BleakError("passive scanning mode requires bluez or_patterns") + @override async def start(self) -> None: manager = await get_global_bluez_manager() @@ -189,6 +141,7 @@ async def start(self) -> None: self._handle_device_removed, ) + @override async def stop(self) -> None: if self._stop: # avoid reentrancy @@ -196,7 +149,7 @@ async def stop(self) -> None: await stop() - def set_scanning_filter(self, **kwargs) -> None: + def set_scanning_filter(self, **kwargs: Any) -> None: """Sets OS level scanning filters for the BleakScanner. For possible values for `filters`, see the parameters to the @@ -264,8 +217,15 @@ def _handle_advertising_data(self, path: str, props: Device1) -> None: ) device = self.create_or_update_device( + path, props["Address"], - props["Alias"], + # BlueZ generates a name based on the address if no name is available. + # To match other backends, we replace this with None. + ( + None + if props["Alias"] == props["Address"].replace(":", "-") + else props["Alias"] + ), {"path": path, "props": props}, advertisement_data, ) @@ -277,8 +237,7 @@ def _handle_device_removed(self, device_path: str) -> None: Handles a device being removed from BlueZ. """ try: - bdaddr = bdaddr_from_device_path(device_path) - del self.seen_devices[bdaddr] + del self.seen_devices[device_path] except KeyError: # The device will not have been added to self.seen_devices if no # advertising data was received, so this is expected to happen diff --git a/bleak/backends/bluezdbus/service.py b/bleak/backends/bluezdbus/service.py deleted file mode 100644 index a1a8d3c27..000000000 --- a/bleak/backends/bluezdbus/service.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Any, List - -from ..service import BleakGATTService -from .characteristic import BleakGATTCharacteristicBlueZDBus -from .utils import extract_service_handle_from_path - - -class BleakGATTServiceBlueZDBus(BleakGATTService): - """GATT Service implementation for the BlueZ DBus backend""" - - def __init__(self, obj: Any, path: str): - super().__init__(obj) - self.__characteristics = [] - self.__path = path - self.__handle = extract_service_handle_from_path(path) - - @property - def uuid(self) -> str: - """The UUID to this service""" - return self.obj["UUID"] - - @property - def handle(self) -> int: - """The integer handle of this service""" - return self.__handle - - @property - def characteristics(self) -> List[BleakGATTCharacteristicBlueZDBus]: - """List of characteristics for this service""" - return self.__characteristics - - def add_characteristic( - self, characteristic: BleakGATTCharacteristicBlueZDBus - ) -> None: - """Add a :py:class:`~BleakGATTCharacteristicBlueZDBus` to the service. - - Should not be used by end user, but rather by `bleak` itself. - """ - self.__characteristics.append(characteristic) - - @property - def path(self) -> str: - """The DBus path. Mostly needed by `bleak`, not by end user""" - return self.__path diff --git a/bleak/backends/bluezdbus/signals.py b/bleak/backends/bluezdbus/signals.py index 6a2ce353f..2e64eb186 100644 --- a/bleak/backends/bluezdbus/signals.py +++ b/bleak/backends/bluezdbus/signals.py @@ -1,8 +1,15 @@ # -*- coding: utf-8 -*- from __future__ import annotations +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "linux": + assert False, "This backend is only available on Linux" + import re -from typing import Any, Coroutine, Dict, Optional +from typing import Any, Optional from dbus_fast.aio.message_bus import MessageBus from dbus_fast.errors import InvalidObjectPathError @@ -69,38 +76,38 @@ def __init__( path_namespace: Optional[str] = None, destination: Optional[str] = None, arg0namespace: Optional[str] = None, - **kwargs, + **kwargs: Any, ): assert_bus_name_valid(type) self.type: str = type if sender: assert_bus_name_valid(sender) - self.sender: str = sender + self.sender: Optional[str] = sender else: self.sender = None if interface: assert_interface_name_valid(interface) - self.interface: str = interface + self.interface: Optional[str] = interface else: self.interface = None if member: assert_member_name_valid(member) - self.member: str = member + self.member: Optional[str] = member else: self.member = None if path: assert_object_path_valid(path) - self.path: str = path + self.path: Optional[str] = path else: self.path = None if path_namespace: assert_object_path_valid(path_namespace) - self.path_namespace: str = path_namespace + self.path_namespace: Optional[str] = path_namespace else: self.path_namespace = None @@ -111,13 +118,13 @@ def __init__( if destination: assert_bus_name_valid(destination) - self.destination: str = destination + self.destination: Optional[str] = destination else: self.destination = None if arg0namespace: assert_bus_name_valid(arg0namespace) - self.arg0namespace: str = arg0namespace + self.arg0namespace: Optional[str] = arg0namespace else: self.arg0namespace = None @@ -132,7 +139,7 @@ def __init__( assert_object_path_valid(v[:-1] if v.endswith("/") else v) else: raise ValueError("kwargs must be in the form 'arg0' or 'arg0path'") - self.args: Dict[str, str] = kwargs + self.args: Optional[dict[str, str]] = kwargs else: self.args = None @@ -174,9 +181,9 @@ def __repr__(self) -> str: return f"MatchRules({self})" -def add_match(bus: MessageBus, rules: MatchRules) -> Coroutine[Any, Any, Message]: +async def add_match(bus: MessageBus, rules: MatchRules) -> Message: """Calls org.freedesktop.DBus.AddMatch using ``rules``.""" - return bus.call( + reply = await bus.call( Message( destination="org.freedesktop.DBus", interface="org.freedesktop.DBus", @@ -186,11 +193,14 @@ def add_match(bus: MessageBus, rules: MatchRules) -> Coroutine[Any, Any, Message body=[str(rules)], ) ) + assert reply + return reply -def remove_match(bus: MessageBus, rules: MatchRules) -> Coroutine[Any, Any, Message]: + +async def remove_match(bus: MessageBus, rules: MatchRules) -> Message: """Calls org.freedesktop.DBus.RemoveMatch using ``rules``.""" - return bus.call( + reply = await bus.call( Message( destination="org.freedesktop.DBus", interface="org.freedesktop.DBus", @@ -200,3 +210,6 @@ def remove_match(bus: MessageBus, rules: MatchRules) -> Coroutine[Any, Any, Mess body=[str(rules)], ) ) + assert reply + + return reply diff --git a/bleak/backends/bluezdbus/utils.py b/bleak/backends/bluezdbus/utils.py index e1acf99f4..46a9879f2 100644 --- a/bleak/backends/bluezdbus/utils.py +++ b/bleak/backends/bluezdbus/utils.py @@ -1,4 +1,10 @@ -# -*- coding: utf-8 -*- +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "linux": + assert False, "This backend is only available on Linux" + import os from typing import Optional @@ -6,7 +12,7 @@ from dbus_fast.constants import MessageType from dbus_fast.message import Message -from ...exc import BleakDBusError, BleakError +from bleak.exc import BleakDBusError, BleakError def assert_reply(reply: Message) -> None: @@ -17,6 +23,7 @@ def assert_reply(reply: Message) -> None: AssertionError: if the message type is not ``MessageType.METHOD_RETURN`` """ if reply.message_type == MessageType.ERROR: + assert reply.error_name raise BleakDBusError(reply.error_name, reply.body) assert reply.message_type == MessageType.METHOD_RETURN @@ -28,19 +35,6 @@ def extract_service_handle_from_path(path: str) -> int: raise BleakError(f"Could not parse service handle from path: {path}") from e -def bdaddr_from_device_path(device_path: str) -> str: - """ - Scrape the Bluetooth address from a D-Bus device path. - - Args: - device_path: The D-Bus object path of the device. - - Returns: - A Bluetooth address as a string. - """ - return ":".join(device_path[-17:].split("_")) - - def device_path_from_characteristic_path(characteristic_path: str) -> str: """ Scrape the device path from a D-Bus characteristic path. @@ -52,7 +46,7 @@ def device_path_from_characteristic_path(characteristic_path: str) -> str: A D-Bus object path of the device. """ # /org/bluez/hci1/dev_FA_23_9D_AA_45_46/service000c/char000d - return characteristic_path[:37] + return characteristic_path[:-21] def get_dbus_authenticator() -> Optional[AuthExternal]: diff --git a/bleak/backends/bluezdbus/version.py b/bleak/backends/bluezdbus/version.py index 40842cb96..fb0edafa5 100644 --- a/bleak/backends/bluezdbus/version.py +++ b/bleak/backends/bluezdbus/version.py @@ -1,3 +1,10 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "linux": + assert False, "This backend is only available on Linux" + import asyncio import contextlib import logging @@ -7,12 +14,13 @@ logger = logging.getLogger(__name__) -async def _get_bluetoothctl_version() -> Optional[re.Match]: +async def _get_bluetoothctl_version() -> Optional[re.Match[bytes]]: """Get the version of bluetoothctl.""" with contextlib.suppress(Exception): proc = await asyncio.create_subprocess_exec( "bluetoothctl", "--version", stdout=asyncio.subprocess.PIPE ) + assert proc.stdout out = await proc.stdout.read() version = re.search(b"(\\d+).(\\d+)", out.strip(b"'")) await proc.wait() @@ -25,10 +33,6 @@ class BlueZFeatures: checked_bluez_version = False supported_version = True - can_write_without_response = True - write_without_response_workaround_needed = False - hides_battery_characteristic = True - hides_device_name_characteristic = True _check_bluez_event: Optional[asyncio.Event] = None @classmethod @@ -43,19 +47,13 @@ async def check_bluez_version(cls) -> None: version_output = await _get_bluetoothctl_version() if version_output: major, minor = tuple(map(int, version_output.groups())) - cls.supported_version = major == 5 and minor >= 34 - cls.can_write_without_response = major == 5 and minor >= 46 - cls.write_without_response_workaround_needed = not ( - major == 5 and minor >= 51 - ) - cls.hides_battery_characteristic = major == 5 and minor >= 48 and minor < 55 - cls.hides_device_name_characteristic = major == 5 and minor >= 48 + cls.supported_version = major == 5 and minor >= 55 else: # Its possible they may be running inside a container where # bluetoothctl is not available and they only have access to the # BlueZ D-Bus API. logging.warning( - "Could not determine BlueZ version, bluetoothctl not available, assuming 5.51+" + "Could not determine BlueZ version, bluetoothctl not available, assuming 5.55+" ) cls._check_bluez_event.set() diff --git a/bleak/backends/characteristic.py b/bleak/backends/characteristic.py index eca52d5ee..15bf8e3ed 100644 --- a/bleak/backends/characteristic.py +++ b/bleak/backends/characteristic.py @@ -1,17 +1,22 @@ # -*- coding: utf-8 -*- +# Created on 2019-03-19 by hbldh """ Interface class for the Bleak representation of a GATT Characteristic - -Created on 2019-03-19 by hbldh - """ -import abc +from __future__ import annotations + import enum -from typing import Any, Callable, List, Union +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Union from uuid import UUID -from ..uuids import uuidstr_to_str -from .descriptor import BleakGATTDescriptor +from bleak.assigned_numbers import CharacteristicPropertyName +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.uuids import normalize_uuid_str, uuidstr_to_str + +# to prevent circular import +if TYPE_CHECKING: + from bleak.backends.service import BleakGATTService class GattCharacteristicsFlags(enum.Enum): @@ -27,10 +32,18 @@ class GattCharacteristicsFlags(enum.Enum): writable_auxiliaries = 0x0200 -class BleakGATTCharacteristic(abc.ABC): - """Interface for the Bleak representation of a GATT Characteristic""" +class BleakGATTCharacteristic: + """The Bleak representation of a GATT Characteristic""" - def __init__(self, obj: Any, max_write_without_response_size: Callable[[], int]): + def __init__( + self, + obj: Any, + handle: int, + uuid: str, + properties: list[CharacteristicPropertyName], + max_write_without_response_size: Callable[[], int], + service: BleakGATTService, + ): """ Args: obj: @@ -38,36 +51,39 @@ def __init__(self, obj: Any, max_write_without_response_size: Callable[[], int]) max_write_without_response_size: The maximum size in bytes that can be written to the characteristic in a single write without response command. + service: + The service this characteristic belongs to. """ self.obj = obj + self._handle = handle + self._uuid = uuid + self._properties = properties self._max_write_without_response_size = max_write_without_response_size + self._service = service + self._descriptors: dict[int, BleakGATTDescriptor] = {} def __str__(self): return f"{self.uuid} (Handle: {self.handle}): {self.description}" @property - @abc.abstractmethod def service_uuid(self) -> str: """The UUID of the Service containing this characteristic""" - raise NotImplementedError() + return self._service.uuid @property - @abc.abstractmethod def service_handle(self) -> int: """The integer handle of the Service containing this characteristic""" - raise NotImplementedError() + return self._service.handle @property - @abc.abstractmethod def handle(self) -> int: """The handle for this characteristic""" - raise NotImplementedError() + return self._handle @property - @abc.abstractmethod def uuid(self) -> str: """The UUID for this characteristic""" - raise NotImplementedError() + return self._uuid @property def description(self) -> str: @@ -75,10 +91,9 @@ def description(self) -> str: return uuidstr_to_str(self.uuid) @property - @abc.abstractmethod - def properties(self) -> List[str]: + def properties(self) -> list[CharacteristicPropertyName]: """Properties of this characteristic""" - raise NotImplementedError() + return self._properties @property def max_write_without_response_size(self) -> int: @@ -112,22 +127,32 @@ def max_write_without_response_size(self) -> int: return self._max_write_without_response_size() @property - @abc.abstractmethod - def descriptors(self) -> List[BleakGATTDescriptor]: + def descriptors(self) -> list[BleakGATTDescriptor]: """List of descriptors for this service""" - raise NotImplementedError() + return list(self._descriptors.values()) - @abc.abstractmethod def get_descriptor( self, specifier: Union[int, str, UUID] ) -> Union[BleakGATTDescriptor, None]: """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" - raise NotImplementedError() + if isinstance(specifier, int): + return self._descriptors.get(specifier) + + uuid = normalize_uuid_str(str(specifier)) + for descriptor in self._descriptors.values(): + if descriptor.uuid == uuid: + return descriptor + + return None - @abc.abstractmethod def add_descriptor(self, descriptor: BleakGATTDescriptor) -> None: """Add a :py:class:`~BleakGATTDescriptor` to the characteristic. Should not be used by end user, but rather by `bleak` itself. """ - raise NotImplementedError() + if descriptor.handle in self._descriptors: + raise ValueError( + f"Descriptor with handle {descriptor.handle} already exists" + ) + + self._descriptors[descriptor.handle] = descriptor diff --git a/bleak/backends/client.py b/bleak/backends/client.py index ddf77f2f3..fdf07b458 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -1,28 +1,25 @@ # -*- coding: utf-8 -*- +# Created on 2018-04-23 by hbldh """ Base class for backend clients. - -Created on 2018-04-23 by hbldh - """ import abc -import asyncio import os import platform import sys -import uuid -from typing import Callable, Optional, Type, Union -from warnings import warn +from collections.abc import Callable +from typing import Any, Optional, Union if sys.version_info < (3, 12): from typing_extensions import Buffer else: from collections.abc import Buffer -from ..exc import BleakError -from .characteristic import BleakGATTCharacteristic -from .device import BLEDevice -from .service import BleakGATTServiceCollection +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.backends.device import BLEDevice +from bleak.backends.service import BleakGATTServiceCollection +from bleak.exc import BleakError NotifyCallback = Callable[[bytearray], None] @@ -42,7 +39,7 @@ class BaseBleakClient(abc.ABC): argument, which will be this client object. """ - def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): + def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs: Any): if isinstance(address_or_ble_device, BLEDevice): self.address = address_or_ble_device.address else: @@ -64,7 +61,7 @@ def mtu_size(self) -> int: # Connectivity methods def set_disconnected_callback( - self, callback: Optional[Callable[[], None]], **kwargs + self, callback: Optional[Callable[[], None]], **kwargs: Any ) -> None: """Set the disconnect callback. The callback will only be called on unsolicited disconnect event. @@ -78,32 +75,30 @@ def set_disconnected_callback( self._disconnected_callback = callback @abc.abstractmethod - async def connect(self, **kwargs) -> bool: + async def connect(self, pair: bool, **kwargs: Any) -> None: """Connect to the specified GATT server. - Returns: - Boolean representing connection status. + Args: + pair (bool): If the client should attempt to pair with the + peripheral before connecting if it is not already paired. + Backends that can't implement this should make an appropriate + log message and ignore the parameter. """ raise NotImplementedError() @abc.abstractmethod - async def disconnect(self) -> bool: - """Disconnect from the specified GATT server. - - Returns: - Boolean representing connection status. - - """ + async def disconnect(self) -> None: + """Disconnect from the specified GATT server.""" raise NotImplementedError() @abc.abstractmethod - async def pair(self, *args, **kwargs) -> bool: + async def pair(self, *args: Any, **kwargs: Any) -> None: """Pair with the peripheral.""" raise NotImplementedError() @abc.abstractmethod - async def unpair(self) -> bool: + async def unpair(self) -> None: """Unpair with the peripheral.""" raise NotImplementedError() @@ -118,54 +113,16 @@ def is_connected(self) -> bool: """ raise NotImplementedError() - class _DeprecatedIsConnectedReturn: - """Wrapper for ``is_connected`` return value to provide deprecation warning.""" - - def __init__(self, value: bool): - self._value = value - - def __bool__(self): - return self._value - - def __call__(self) -> bool: - warn( - "is_connected has been changed to a property. Calling it as an async method will be removed in a future version", - FutureWarning, - stacklevel=2, - ) - f = asyncio.Future() - f.set_result(self._value) - return f - - def __repr__(self) -> str: - return repr(self._value) - - # GATT services methods - - @abc.abstractmethod - async def get_services(self, **kwargs) -> BleakGATTServiceCollection: - """Get all services registered for this GATT server. - - Returns: - A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. - - """ - raise NotImplementedError() - # I/O methods @abc.abstractmethod async def read_gatt_char( - self, - char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - **kwargs, + self, characteristic: BleakGATTCharacteristic, **kwargs: Any ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from, - specified by either integer handle, UUID or directly by the - BleakGATTCharacteristic object representing it. + characteristic (BleakGATTCharacteristic): The characteristic to read from. Returns: (bytearray) The read data. @@ -174,24 +131,23 @@ async def read_gatt_char( raise NotImplementedError() @abc.abstractmethod - async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: + async def read_gatt_descriptor( + self, descriptor: BleakGATTDescriptor, **kwargs: Any + ) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: - handle (int): The handle of the descriptor to read from. + descriptor: The descriptor to read from. Returns: - (bytearray) The read data. + The read data. """ raise NotImplementedError() @abc.abstractmethod async def write_gatt_char( - self, - characteristic: BleakGATTCharacteristic, - data: Buffer, - response: bool, + self, characteristic: BleakGATTCharacteristic, data: Buffer, response: bool ) -> None: """ Perform a write operation on the specified GATT characteristic. @@ -204,11 +160,13 @@ async def write_gatt_char( raise NotImplementedError() @abc.abstractmethod - async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: + async def write_gatt_descriptor( + self, descriptor: BleakGATTDescriptor, data: Buffer + ) -> None: """Perform a write operation on the specified GATT descriptor. Args: - handle: The handle of the descriptor to read from. + descriptor: The descriptor to read from. data: The data to send (any bytes-like object). """ @@ -219,7 +177,7 @@ async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, - **kwargs, + **kwargs: Any, ) -> None: """ Activate notifications/indications on a characteristic. @@ -233,21 +191,18 @@ async def start_notify( raise NotImplementedError() @abc.abstractmethod - async def stop_notify( - self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID] - ) -> None: + async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None: """Deactivate notification/indication on a specified characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate - notification/indication on, specified by either integer handle, UUID or - directly by the BleakGATTCharacteristic object representing it. + characteristic (BleakGATTCharacteristic): The characteristic to deactivate + notification/indication on. """ raise NotImplementedError() -def get_platform_client_backend_type() -> Type[BaseBleakClient]: +def get_platform_client_backend_type() -> type[BaseBleakClient]: """ Gets the platform-specific :class:`BaseBleakClient` type. """ diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 6b8766414..efd8c400f 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -1,21 +1,28 @@ +# Created on June, 25 2019 by kevincar """ CentralManagerDelegate will implement the CBCentralManagerDelegate protocol to manage CoreBluetooth services and resources on the Central End +""" -Created on June, 25 2019 by kevincar +import sys +from typing import TYPE_CHECKING -""" +if TYPE_CHECKING: + if sys.platform != "darwin": + assert False, "This backend is only available on macOS" import asyncio import logging -import sys import threading -from typing import Any, Callable, Dict, List, Optional +from collections.abc import Callable +from typing import Any, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout + from typing_extensions import Self else: from asyncio import timeout as async_timeout + from typing import Self import objc from CoreBluetooth import ( @@ -36,13 +43,11 @@ NSError, NSKeyValueChangeNewKey, NSKeyValueObservingOptionNew, - NSNumber, NSObject, - NSString, ) from libdispatch import DISPATCH_QUEUE_SERIAL, dispatch_queue_create -from ...exc import BleakError +from bleak.exc import BleakError logger = logging.getLogger(__name__) CBCentralManagerDelegate = objc.protocolNamed("CBCentralManagerDelegate") @@ -56,7 +61,7 @@ class CentralManagerDelegate(NSObject): ___pyobjc_protocols__ = [CBCentralManagerDelegate] - def init(self) -> Optional["CentralManagerDelegate"]: + def init(self: Self) -> Optional[Self]: """macOS init function for NSObject""" self = objc.super(CentralManagerDelegate, self).init() @@ -64,13 +69,14 @@ def init(self) -> Optional["CentralManagerDelegate"]: return None self.event_loop = asyncio.get_running_loop() - self._connect_futures: Dict[NSUUID, asyncio.Future] = {} + self._connect_futures: dict[NSUUID, asyncio.Future[bool]] = {} - self.callbacks: Dict[ - int, Callable[[CBPeripheral, Dict[str, Any], int], None] + self.callbacks: dict[ + int, + Callable[[CBPeripheral, NSDictionary, int], None], ] = {} - self._disconnect_callbacks: Dict[NSUUID, DisconnectCallback] = {} - self._disconnect_futures: Dict[NSUUID, asyncio.Future] = {} + self._disconnect_callbacks: dict[NSUUID, DisconnectCallback] = {} + self._disconnect_futures: dict[NSUUID, asyncio.Future[None]] = {} self._did_update_state_event = threading.Event() self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_( @@ -93,65 +99,52 @@ def init(self) -> Optional["CentralManagerDelegate"]: if self.central_manager.state() != CBManagerStatePoweredOn: raise BleakError("Bluetooth device is turned off") - # isScanning property was added in 10.13 - if objc.macos_available(10, 13): - self.central_manager.addObserver_forKeyPath_options_context_( - self, "isScanning", NSKeyValueObservingOptionNew, 0 - ) - self._did_start_scanning_event: Optional[asyncio.Event] = None - self._did_stop_scanning_event: Optional[asyncio.Event] = None + self.central_manager.addObserver_forKeyPath_options_context_( + self, "isScanning", NSKeyValueObservingOptionNew, 0 + ) + self._did_start_scanning_event: Optional[asyncio.Event] = None + self._did_stop_scanning_event: Optional[asyncio.Event] = None return self def __del__(self) -> None: - if objc.macos_available(10, 13): - try: - self.central_manager.removeObserver_forKeyPath_(self, "isScanning") - except IndexError: - # If self.init() raised an exception before calling - # addObserver_forKeyPath_options_context_, attempting - # to remove the observer will fail with IndexError - pass + try: + self.central_manager.removeObserver_forKeyPath_(self, "isScanning") + except IndexError: + # If self.init() raised an exception before calling + # addObserver_forKeyPath_options_context_, attempting + # to remove the observer will fail with IndexError + pass # User defined functions @objc.python_method - async def start_scan(self, service_uuids: Optional[List[str]]) -> None: - service_uuids = ( - NSArray.alloc().initWithArray_( - list(map(CBUUID.UUIDWithString_, service_uuids)) - ) + async def start_scan(self, service_uuids: Optional[list[str]]) -> None: + _service_uuids = ( + NSArray[CBUUID] + .alloc() + .initWithArray_(list(map(CBUUID.UUIDWithString_, service_uuids))) if service_uuids else None ) self.central_manager.scanForPeripheralsWithServices_options_( - service_uuids, None + _service_uuids, None ) - # The `isScanning` property was added in macOS 10.13, so before that - # just waiting some will have to do. - if objc.macos_available(10, 13): - event = asyncio.Event() - self._did_start_scanning_event = event - if not self.central_manager.isScanning(): - await event.wait() - else: - await asyncio.sleep(0.1) + event = asyncio.Event() + self._did_start_scanning_event = event + if not self.central_manager.isScanning(): + await event.wait() @objc.python_method async def stop_scan(self) -> None: self.central_manager.stopScan() - # The `isScanning` property was added in macOS 10.13, so before that - # just waiting some will have to do. - if objc.macos_available(10, 13): - event = asyncio.Event() - self._did_stop_scanning_event = event - if self.central_manager.isScanning(): - await event.wait() - else: - await asyncio.sleep(0.1) + event = asyncio.Event() + self._did_stop_scanning_event = event + if self.central_manager.isScanning(): + await event.wait() @objc.python_method async def connect( @@ -207,7 +200,7 @@ def _changed_is_scanning(self, is_scanning: bool) -> None: self._did_stop_scanning_event.set() def observeValueForKeyPath_ofObject_change_context_( - self, keyPath: NSString, object: Any, change: NSDictionary, context: int + self, keyPath: str, object: Any, change: NSDictionary, context: int ) -> None: logger.debug("'%s' changed", keyPath) @@ -242,7 +235,7 @@ def did_discover_peripheral( central: CBCentralManager, peripheral: CBPeripheral, advertisementData: NSDictionary, - RSSI: NSNumber, + RSSI: int, ) -> None: # Note: this function might be called several times for same device. # This can happen for instance when an active scan is done, and the @@ -278,7 +271,7 @@ def centralManager_didDiscoverPeripheral_advertisementData_RSSI_( central: CBCentralManager, peripheral: CBPeripheral, advertisementData: NSDictionary, - RSSI: NSNumber, + RSSI: int, ) -> None: logger.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_") self.event_loop.call_soon_threadsafe( diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index d48f477c8..825dfed64 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -8,11 +8,18 @@ from __future__ import annotations +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "darwin": + assert False, "This backend is only available on macOS" + import asyncio import itertools import logging -import sys -from typing import Any, Dict, Iterable, NewType, Optional +from collections.abc import Iterable +from typing import Any, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout @@ -21,23 +28,25 @@ import objc from CoreBluetooth import ( + CBUUID, CBCharacteristic, + CBCharacteristicWriteType, CBCharacteristicWriteWithResponse, CBDescriptor, CBPeripheral, CBService, ) -from Foundation import NSUUID, NSArray, NSData, NSError, NSNumber, NSObject, NSString +from Foundation import NSUUID, NSArray, NSData, NSError, NSObject -from ...exc import BleakError -from ..client import NotifyCallback +from bleak.args.corebluetooth import NotificationDiscriminator +from bleak.backends.client import NotifyCallback +from bleak.exc import BleakError # logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) -CBPeripheralDelegate = objc.protocolNamed("CBPeripheralDelegate") -CBCharacteristicWriteType = NewType("CBCharacteristicWriteType", int) +CBPeripheralDelegate = objc.protocolNamed("CBPeripheralDelegate") class PeripheralDelegate(NSObject): @@ -60,24 +69,31 @@ def initWithPeripheral_( self._event_loop = asyncio.get_running_loop() self._services_discovered_future = self._event_loop.create_future() - self._service_characteristic_discovered_futures: Dict[int, asyncio.Future] = {} - self._characteristic_descriptor_discover_futures: Dict[int, asyncio.Future] = {} + self._service_characteristic_discovered_futures: dict[ + int, asyncio.Future[NSArray[CBCharacteristic]] + ] = {} + self._characteristic_descriptor_discover_futures: dict[ + int, asyncio.Future[None] + ] = {} - self._characteristic_read_futures: Dict[int, asyncio.Future] = {} - self._characteristic_write_futures: Dict[int, asyncio.Future] = {} + self._characteristic_read_futures: dict[int, asyncio.Future[NSData]] = {} + self._characteristic_write_futures: dict[int, asyncio.Future[None]] = {} - self._descriptor_read_futures: Dict[int, asyncio.Future] = {} - self._descriptor_write_futures: Dict[int, asyncio.Future] = {} + self._descriptor_read_futures: dict[int, asyncio.Future[NSObject]] = {} + self._descriptor_write_futures: dict[int, asyncio.Future[None]] = {} - self._characteristic_notify_change_futures: Dict[int, asyncio.Future] = {} - self._characteristic_notify_callbacks: Dict[int, NotifyCallback] = {} + self._characteristic_notify_change_futures: dict[int, asyncio.Future[None]] = {} + self._characteristic_notify_callbacks: dict[int, NotifyCallback] = {} + self._characteristic_notification_discriminators: dict[ + int, Optional[NotificationDiscriminator] + ] = {} - self._read_rssi_futures: Dict[NSUUID, asyncio.Future] = {} + self._read_rssi_futures: dict[NSUUID, asyncio.Future[int]] = {} return self @objc.python_method - def futures(self) -> Iterable[asyncio.Future]: + def futures(self) -> Iterable[asyncio.Future[Any]]: """ Gets all futures for this delegate. @@ -102,7 +118,9 @@ def futures(self) -> Iterable[asyncio.Future]: ) @objc.python_method - async def discover_services(self, services: Optional[NSArray]) -> NSArray: + async def discover_services( + self, services: Optional[NSArray[CBUUID]] = None + ) -> NSArray[CBService]: future = self._event_loop.create_future() self._services_discovered_future = future @@ -113,7 +131,9 @@ async def discover_services(self, services: Optional[NSArray]) -> NSArray: del self._services_discovered_future @objc.python_method - async def discover_characteristics(self, service: CBService) -> NSArray: + async def discover_characteristics( + self, service: CBService + ) -> NSArray[CBCharacteristic]: future = self._event_loop.create_future() self._service_characteristic_discovered_futures[service.startHandle()] = future @@ -124,7 +144,9 @@ async def discover_characteristics(self, service: CBService) -> NSArray: del self._service_characteristic_discovered_futures[service.startHandle()] @objc.python_method - async def discover_descriptors(self, characteristic: CBCharacteristic) -> NSArray: + async def discover_descriptors( + self, characteristic: CBCharacteristic + ) -> NSArray[CBDescriptor]: future = self._event_loop.create_future() self._characteristic_descriptor_discover_futures[characteristic.handle()] = ( @@ -144,15 +166,17 @@ async def discover_descriptors(self, characteristic: CBCharacteristic) -> NSArra async def read_characteristic( self, characteristic: CBCharacteristic, - use_cached: bool = True, + use_cached: bool, timeout: int = 20, ) -> NSData: - if characteristic.value() is not None and use_cached: - return characteristic.value() + value = characteristic.value() + if value is not None and use_cached: + return value future = self._event_loop.create_future() self._characteristic_read_futures[characteristic.handle()] = future + try: self.peripheral.readValueForCharacteristic_(characteristic) async with async_timeout(timeout): @@ -164,8 +188,9 @@ async def read_characteristic( async def read_descriptor( self, descriptor: CBDescriptor, use_cached: bool = True ) -> Any: - if descriptor.value() is not None and use_cached: - return descriptor.value() + value = descriptor.value() + if value is not None and use_cached: + return value future = self._event_loop.create_future() @@ -214,13 +239,19 @@ async def write_descriptor(self, descriptor: CBDescriptor, value: NSData) -> Non @objc.python_method async def start_notifications( - self, characteristic: CBCharacteristic, callback: NotifyCallback + self, + characteristic: CBCharacteristic, + callback: NotifyCallback, + notification_discriminator: Optional[NotificationDiscriminator] = None, ) -> None: c_handle = characteristic.handle() if c_handle in self._characteristic_notify_callbacks: raise ValueError("Characteristic notifications already started") self._characteristic_notify_callbacks[c_handle] = callback + self._characteristic_notification_discriminators[c_handle] = ( + notification_discriminator + ) future = self._event_loop.create_future() @@ -247,9 +278,10 @@ async def stop_notifications(self, characteristic: CBCharacteristic) -> None: del self._characteristic_notify_change_futures[c_handle] self._characteristic_notify_callbacks.pop(c_handle) + self._characteristic_notification_discriminators.pop(c_handle) @objc.python_method - async def read_rssi(self) -> NSNumber: + async def read_rssi(self) -> int: future = self._event_loop.create_future() self._read_rssi_futures[self.peripheral.identifier()] = future @@ -263,7 +295,10 @@ async def read_rssi(self) -> NSNumber: @objc.python_method def did_discover_services( - self, peripheral: CBPeripheral, services: NSArray, error: Optional[NSError] + self, + peripheral: CBPeripheral, + services: NSArray[CBService], + error: Optional[NSError], ) -> None: future = self._services_discovered_future if error is not None: @@ -289,7 +324,7 @@ def did_discover_characteristics_for_service( self, peripheral: CBPeripheral, service: CBService, - characteristics: NSArray, + characteristics: NSArray[CBCharacteristic], error: Optional[NSError], ) -> None: future = self._service_characteristic_discovered_futures.get( @@ -364,21 +399,40 @@ def did_update_value_for_characteristic( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, - value: NSData, + value: Optional[NSData], error: Optional[NSError], ) -> None: c_handle = characteristic.handle() future = self._characteristic_read_futures.get(c_handle) - # If there is no pending read request, then this must be a notification - # (the same delegate callback is used by both). - if not future: - if error is None: + # If error is set, then we know this was a read response. + # Otherwise, if there is a pending read request, we can't tell if this is a read response or notification. + # If the user provided a notification discriminator, we can use that to + # identify if this callback is due to a notification by analyzing the value. + # If not, and there is a future (pending read request), we assume it is a read response but can't know for sure. + if not error: + assert value is not None + + notification_discriminator = ( + self._characteristic_notification_discriminators.get(c_handle) + ) + if not future or ( + notification_discriminator and notification_discriminator(bytes(value)) + ): notify_callback = self._characteristic_notify_callbacks.get(c_handle) if notify_callback: notify_callback(bytearray(value)) + return + + if not future: + logger.warning( + "Unexpected event didUpdateValueForCharacteristic for 0x%04x with value: %r and error: %r", + c_handle, + value, + error, + ) return if error is not None: @@ -386,6 +440,7 @@ def did_update_value_for_characteristic( future.set_exception(exception) else: logger.debug("Read characteristic value") + assert value is not None future.set_result(value) def peripheral_didUpdateValueForCharacteristic_error_( @@ -408,7 +463,7 @@ def did_update_value_for_descriptor( self, peripheral: CBPeripheral, descriptor: CBDescriptor, - value: NSObject, + value: Optional[Any], error: Optional[NSError], ) -> None: future = self._descriptor_read_futures.get(descriptor.handle()) @@ -422,6 +477,7 @@ def did_update_value_for_descriptor( future.set_exception(exception) else: logger.debug("Read descriptor value") + assert value is not None future.set_result(value) def peripheral_didUpdateValueForDescriptor_error_( @@ -545,7 +601,7 @@ def peripheral_didUpdateNotificationStateForCharacteristic_error_( @objc.python_method def did_read_rssi( - self, peripheral: CBPeripheral, rssi: NSNumber, error: Optional[NSError] + self, peripheral: CBPeripheral, rssi: int, error: Optional[NSError] ) -> None: future = self._read_rssi_futures.get(peripheral.identifier(), None) @@ -559,12 +615,21 @@ def did_read_rssi( else: future.set_result(rssi) - # peripheral_didReadRSSI_error_ method is added dynamically later + def peripheral_didReadRSSI_error_( + self: PeripheralDelegate, + peripheral: CBPeripheral, + rssi: int, + error: Optional[NSError], + ) -> None: + logger.debug("peripheral_didReadRSSI_error_") + self._event_loop.call_soon_threadsafe( + self.did_read_rssi, peripheral, rssi, error + ) # Bleak currently doesn't use the callbacks below other than for debug logging @objc.python_method - def did_update_name(self, peripheral: CBPeripheral, name: NSString) -> None: + def did_update_name(self, peripheral: CBPeripheral, name: str) -> None: logger.debug(f"name of {peripheral.identifier()} changed to {name}") def peripheralDidUpdateName_(self, peripheral: CBPeripheral) -> None: @@ -575,55 +640,16 @@ def peripheralDidUpdateName_(self, peripheral: CBPeripheral) -> None: @objc.python_method def did_modify_services( - self, peripheral: CBPeripheral, invalidated_services: NSArray + self, peripheral: CBPeripheral, invalidated_services: NSArray[CBService] ) -> None: logger.debug( f"{peripheral.identifier()} invalidated services: {invalidated_services}" ) def peripheral_didModifyServices_( - self, peripheral: CBPeripheral, invalidatedServices: NSArray + self, peripheral: CBPeripheral, invalidatedServices: NSArray[CBService] ) -> None: logger.debug("peripheral_didModifyServices_") self._event_loop.call_soon_threadsafe( self.did_modify_services, peripheral, invalidatedServices ) - - -# peripheralDidUpdateRSSI:error: was deprecated and replaced with -# peripheral:didReadRSSI:error: in macOS 10.13 -if objc.macos_available(10, 13): - - def peripheral_didReadRSSI_error_( - self: PeripheralDelegate, - peripheral: CBPeripheral, - rssi: NSNumber, - error: Optional[NSError], - ) -> None: - logger.debug("peripheral_didReadRSSI_error_") - self._event_loop.call_soon_threadsafe( - self.did_read_rssi, peripheral, rssi, error - ) - - objc.classAddMethod( - PeripheralDelegate, - b"peripheral:didReadRSSI:error:", - peripheral_didReadRSSI_error_, - ) - - -else: - - def peripheralDidUpdateRSSI_error_( - self: PeripheralDelegate, peripheral: CBPeripheral, error: Optional[NSError] - ) -> None: - logger.debug("peripheralDidUpdateRSSI_error_") - self._event_loop.call_soon_threadsafe( - self.did_read_rssi, peripheral, peripheral.RSSI(), error - ) - - objc.classAddMethod( - PeripheralDelegate, - b"peripheralDidUpdateRSSI:error:", - peripheralDidUpdateRSSI_error_, - ) diff --git a/bleak/backends/corebluetooth/__init__.py b/bleak/backends/corebluetooth/__init__.py index ed160a3dd..696ae6aa7 100644 --- a/bleak/backends/corebluetooth/__init__.py +++ b/bleak/backends/corebluetooth/__init__.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- +# Created on 2017-11-19 by hbldh """ __init__.py - -Created on 2017-11-19 by hbldh - """ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "darwin": + assert False, "This backend is only available on macOS" import objc diff --git a/bleak/backends/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py deleted file mode 100644 index 4bf617115..000000000 --- a/bleak/backends/corebluetooth/characteristic.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -Interface class for the Bleak representation of a GATT Characteristic - -Created on 2019-06-28 by kevincar - -""" - -from enum import Enum -from typing import Callable, Dict, List, Optional, Tuple, Union - -from CoreBluetooth import CBCharacteristic - -from ..characteristic import BleakGATTCharacteristic -from ..descriptor import BleakGATTDescriptor -from .descriptor import BleakGATTDescriptorCoreBluetooth -from .utils import cb_uuid_to_str - - -class CBCharacteristicProperties(Enum): - BROADCAST = 0x1 - READ = 0x2 - WRITE_WITHOUT_RESPONSE = 0x4 - WRITE = 0x8 - NOTIFY = 0x10 - INDICATE = 0x20 - AUTHENTICATED_SIGNED_WRITES = 0x40 - EXTENDED_PROPERTIES = 0x80 - NOTIFY_ENCRYPTION_REQUIRED = 0x100 - INDICATE_ENCRYPTION_REQUIRED = 0x200 - - -_GattCharacteristicsPropertiesEnum: Dict[Optional[int], Tuple[str, str]] = { - None: ("None", "The characteristic doesn’t have any properties that apply"), - 1: ("Broadcast".lower(), "The characteristic supports broadcasting"), - 2: ("Read".lower(), "The characteristic is readable"), - 4: ( - "Write-Without-Response".lower(), - "The characteristic supports Write Without Response", - ), - 8: ("Write".lower(), "The characteristic is writable"), - 16: ("Notify".lower(), "The characteristic is notifiable"), - 32: ("Indicate".lower(), "The characteristic is indicatable"), - 64: ( - "Authenticated-Signed-Writes".lower(), - "The characteristic supports signed writes", - ), - 128: ( - "Extended-Properties".lower(), - "The ExtendedProperties Descriptor is present", - ), - 256: ("Reliable-Writes".lower(), "The characteristic supports reliable writes"), - 512: ( - "Writable-Auxiliaries".lower(), - "The characteristic has writable auxiliaries", - ), -} - - -class BleakGATTCharacteristicCoreBluetooth(BleakGATTCharacteristic): - """GATT Characteristic implementation for the CoreBluetooth backend""" - - 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() - self.__props: List[str] = [ - _GattCharacteristicsPropertiesEnum[v][0] - for v in [2**n for n in range(10)] - if (self.obj.properties() & v) - ] - self._uuid: str = cb_uuid_to_str(self.obj.UUID()) - - @property - def service_uuid(self) -> str: - """The uuid of the Service containing this characteristic""" - return cb_uuid_to_str(self.obj.service().UUID()) - - @property - def service_handle(self) -> int: - return int(self.obj.service().startHandle()) - - @property - def handle(self) -> int: - """Integer handle for this characteristic""" - return int(self.obj.handle()) - - @property - def uuid(self) -> str: - """The uuid of this characteristic""" - return self._uuid - - @property - def properties(self) -> List[str]: - """Properties of this characteristic""" - return self.__props - - @property - def descriptors(self) -> List[BleakGATTDescriptor]: - """List of descriptors for this service""" - return self.__descriptors - - def get_descriptor(self, specifier) -> Union[BleakGATTDescriptor, None]: - """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" - try: - if isinstance(specifier, int): - return next(filter(lambda x: x.handle == specifier, self.descriptors)) - else: - return next( - filter(lambda x: x.uuid == str(specifier), self.descriptors) - ) - except StopIteration: - return None - - def add_descriptor(self, descriptor: BleakGATTDescriptor): - """Add a :py:class:`~BleakGATTDescriptor` to the characteristic. - - Should not be used by end user, but rather by `bleak` itself. - """ - self.__descriptors.append(descriptor) diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index a682dadbd..ba27ce7c4 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -1,19 +1,24 @@ +# Created on 2019-06-26 by kevincar """ BLE Client for CoreBluetooth on macOS - -Created on 2019-06-26 by kevincar """ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "darwin": + assert False, "This backend is only available on macOS" + import asyncio import logging -import sys -import uuid -from typing import Optional, Set, Union +from typing import Any, Optional, Union if sys.version_info < (3, 12): - from typing_extensions import Buffer + from typing_extensions import Buffer, override else: from collections.abc import Buffer + from typing import override from CoreBluetooth import ( CBUUID, @@ -24,23 +29,19 @@ ) from Foundation import NSArray, NSData -from ... import BleakScanner -from ...exc import ( - BleakCharacteristicNotFoundError, - BleakDeviceNotFoundError, - BleakError, -) -from ..characteristic import BleakGATTCharacteristic -from ..client import BaseBleakClient, NotifyCallback -from ..device import BLEDevice -from ..service import BleakGATTServiceCollection -from .CentralManagerDelegate import CentralManagerDelegate -from .characteristic import BleakGATTCharacteristicCoreBluetooth -from .descriptor import BleakGATTDescriptorCoreBluetooth -from .PeripheralDelegate import PeripheralDelegate -from .scanner import BleakScannerCoreBluetooth -from .service import BleakGATTServiceCoreBluetooth -from .utils import cb_uuid_to_str +from bleak import BleakScanner +from bleak.args.corebluetooth import CBStartNotifyArgs +from bleak.assigned_numbers import gatt_char_props_to_strs +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, NotifyCallback +from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate +from bleak.backends.corebluetooth.PeripheralDelegate import PeripheralDelegate +from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth +from bleak.backends.corebluetooth.utils import cb_uuid_to_str +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.backends.device import BLEDevice +from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection +from bleak.exc import BleakDeviceNotFoundError, BleakError logger = logging.getLogger(__name__) @@ -60,8 +61,8 @@ class BleakClientCoreBluetooth(BaseBleakClient): def __init__( self, address_or_ble_device: Union[BLEDevice, str], - services: Optional[Set[str]] = None, - **kwargs, + services: Optional[set[str]] = None, + **kwargs: Any, ): super(BleakClientCoreBluetooth, self).__init__(address_or_ble_device, **kwargs) @@ -76,7 +77,9 @@ def __init__( ) = address_or_ble_device.details self._requested_services = ( - NSArray.alloc().initWithArray_(list(map(CBUUID.UUIDWithString_, services))) + NSArray[CBUUID] + .alloc() + .initWithArray_(list(map(CBUUID.UUIDWithString_, services))) if services else None ) @@ -84,16 +87,16 @@ def __init__( def __str__(self) -> str: return "BleakClientCoreBluetooth ({})".format(self.address) - async def connect(self, **kwargs) -> bool: + @override + async def connect(self, pair: bool, **kwargs: Any) -> None: """Connect to a specified Peripheral Keyword Args: timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. - - Returns: - Boolean representing connection status. - """ + if pair: + logger.debug("Explicit pairing is not available in CoreBluetooth.") + timeout = kwargs.get("timeout", self._timeout) if self._peripheral is None: device = await BleakScanner.find_device_by_address( @@ -136,37 +139,38 @@ def disconnect_callback() -> None: await manager.connect(self._peripheral, disconnect_callback, timeout=timeout) # Now get services - await self.get_services() - - return True + await self._get_services() - async def disconnect(self) -> bool: + @override + async def disconnect(self) -> None: """Disconnect from the peripheral device""" if ( self._peripheral is None or self._peripheral.state() != CBPeripheralStateConnected ): - return True + return + assert self._central_manager_delegate await self._central_manager_delegate.disconnect(self._peripheral) - return True - @property + @override def is_connected(self) -> bool: """Checks for current active connection""" - return self._DeprecatedIsConnectedReturn( + return ( False if self._peripheral is None else self._peripheral.state() == CBPeripheralStateConnected ) @property + @override def mtu_size(self) -> int: """Get ATT MTU size for active connection""" # Use type CBCharacteristicWriteWithoutResponse to get maximum write # value length based on the negotiated ATT MTU size. Add the ATT header # length (+3) to get the actual ATT MTU size. + assert self._peripheral return ( self._peripheral.maximumWriteValueLengthForType_( CBCharacteristicWriteWithoutResponse @@ -174,36 +178,37 @@ def mtu_size(self) -> int: + 3 ) - async def pair(self, *args, **kwargs) -> bool: + @override + async def pair(self, *args: Any, **kwargs: Any) -> None: """Attempt to pair with a peripheral. - .. note:: - - This is not available on macOS since there is not explicit method to do a pairing, Instead the docs - state that it "auto-pairs" when trying to read a characteristic that requires encryption, something - Bleak cannot do apparently. + Raises: + NotImplementedError: + This is not available on macOS since there is not explicit API + to do a pairing. Instead, the docs state that it "auto-pairs", + when trying to read a characteristic that requires encryption. Reference: - `Apple Docs `_ - `Stack Overflow post #1 `_ - `Stack Overflow post #2 `_ - - Returns: - Boolean regarding success of pairing. - """ raise NotImplementedError("Pairing is not available in Core Bluetooth.") - async def unpair(self) -> bool: + @override + async def unpair(self) -> None: """ + Remove pairing information for a peripheral. - Returns: - + Raises: + NotImplementedError: + This is not available on macOS since there is not explicit API + to do a pairing. """ raise NotImplementedError("Pairing is not available in Core Bluetooth.") - async def get_services(self, **kwargs) -> BleakGATTServiceCollection: + async def _get_services(self) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. Returns: @@ -216,79 +221,77 @@ async def get_services(self, **kwargs) -> BleakGATTServiceCollection: services = BleakGATTServiceCollection() logger.debug("Retrieving services...") + assert self._delegate cb_services = await self._delegate.discover_services(self._requested_services) for service in cb_services: + serv = BleakGATTService( + service, service.startHandle(), cb_uuid_to_str(service.UUID()) + ) + services.add_service(serv) + serviceUUID = service.UUID().UUIDString() logger.debug( "Retrieving characteristics for service {}".format(serviceUUID) ) characteristics = await self._delegate.discover_characteristics(service) - services.add_service(BleakGATTServiceCoreBluetooth(service)) - for characteristic in characteristics: cUUID = characteristic.UUID().UUIDString() logger.debug( "Retrieving descriptors for characteristic {}".format(cUUID) ) - descriptors = await self._delegate.discover_descriptors(characteristic) - services.add_characteristic( - BleakGATTCharacteristicCoreBluetooth( - characteristic, - lambda: self._peripheral.maximumWriteValueLengthForType_( - CBCharacteristicWriteWithoutResponse - ), - ) + char = BleakGATTCharacteristic( + characteristic, + characteristic.handle(), + cb_uuid_to_str(characteristic.UUID()), + list(gatt_char_props_to_strs(characteristic.properties())), + lambda: self._peripheral.maximumWriteValueLengthForType_( + CBCharacteristicWriteWithoutResponse + ), + serv, ) + services.add_characteristic(char) + + descriptors = await self._delegate.discover_descriptors(characteristic) for descriptor in descriptors: - services.add_descriptor( - BleakGATTDescriptorCoreBluetooth( - descriptor, - cb_uuid_to_str(characteristic.UUID()), - int(characteristic.handle()), - ) + desc = BleakGATTDescriptor( + descriptor, + int(descriptor.handle()), + cb_uuid_to_str(descriptor.UUID()), + char, ) + services.add_descriptor(desc) + logger.debug("Services resolved for %s", str(self)) self.services = services return self.services + @override async def read_gatt_char( - self, - char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - use_cached: bool = False, - **kwargs, + self, characteristic: BleakGATTCharacteristic, **kwargs: Any ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from, - specified by either integer handle, UUID or directly by the - BleakGATTCharacteristic object representing it. - use_cached (bool): `False` forces macOS to read the value from the - device again and not use its own cached value. Defaults to `False`. + characteristic (BleakGATTCharacteristic): The characteristic to read from. Returns: (bytearray) The read data. """ - if not isinstance(char_specifier, BleakGATTCharacteristic): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - if not characteristic: - raise BleakCharacteristicNotFoundError(char_specifier) - + assert self._delegate output = await self._delegate.read_characteristic( - characteristic.obj, use_cached=use_cached + characteristic.obj, use_cached=kwargs.get("use_cached", False) ) value = bytearray(output) logger.debug("Read Characteristic {0} : {1}".format(characteristic.uuid, value)) return value + @override async def read_gatt_descriptor( - self, handle: int, use_cached: bool = False, **kwargs + self, descriptor: BleakGATTDescriptor, **kwargs: Any ) -> bytearray: """Perform read operation on the specified GATT descriptor. @@ -300,12 +303,9 @@ async def read_gatt_descriptor( Returns: (bytearray) The read data. """ - descriptor = self.services.get_descriptor(handle) - if not descriptor: - raise BleakError("Descriptor {} was not found!".format(handle)) - + assert self._delegate output = await self._delegate.read_descriptor( - descriptor.obj, use_cached=use_cached + descriptor.obj, use_cached=kwargs.get("use_cached", False) ) if isinstance( output, str @@ -313,14 +313,12 @@ async def read_gatt_descriptor( value = bytearray(output.encode("utf-8")) else: # _NSInlineData value = bytearray(output) # value.getBytes_length_(None, len(value)) - logger.debug("Read Descriptor {0} : {1}".format(handle, value)) + logger.debug("Read Descriptor %d : %r", descriptor.handle, value) return value + @override async def write_gatt_char( - self, - characteristic: BleakGATTCharacteristic, - data: Buffer, - response: bool, + self, characteristic: BleakGATTCharacteristic, data: Buffer, response: bool ) -> None: value = NSData.alloc().initWithBytes_length_(data, len(data)) await self._delegate.write_characteristic( @@ -334,56 +332,54 @@ async def write_gatt_char( ) logger.debug(f"Write Characteristic {characteristic.uuid} : {data}") - async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: + @override + async def write_gatt_descriptor( + self, descriptor: BleakGATTDescriptor, data: Buffer + ) -> None: """Perform a write operation on the specified GATT descriptor. Args: - handle: The handle of the descriptor to read from. + descriptor: The descriptor to read from. data: The data to send (any bytes-like object). """ - descriptor = self.services.get_descriptor(handle) - if not descriptor: - raise BleakError("Descriptor {} was not found!".format(handle)) - + assert self._delegate value = NSData.alloc().initWithBytes_length_(data, len(data)) await self._delegate.write_descriptor(descriptor.obj, value) - logger.debug("Write Descriptor {0} : {1}".format(handle, data)) + logger.debug("Write Descriptor %d : %r", descriptor.handle, data) + @override async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, - **kwargs, + *, + cb: CBStartNotifyArgs, + **kwargs: Any, ) -> None: """ Activate notifications/indications on a characteristic. """ assert self._delegate is not None - await self._delegate.start_notifications(characteristic.obj, callback) + await self._delegate.start_notifications( + characteristic.obj, + callback, + cb.get("notification_discriminator"), + ) - async def stop_notify( - self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID] - ) -> None: + @override + async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None: """Deactivate notification/indication on a specified characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate - notification/indication on, specified by either integer handle, UUID or - directly by the BleakGATTCharacteristic object representing it. - - + characteristic (BleakGATTCharacteristic: The characteristic to deactivate + notification/indication on. """ - if not isinstance(char_specifier, BleakGATTCharacteristic): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - if not characteristic: - raise BleakCharacteristicNotFoundError(char_specifier) - + assert self._delegate await self._delegate.stop_notifications(characteristic.obj) async def get_rssi(self) -> int: """To get RSSI value in dBm of the connected Peripheral""" + assert self._delegate return int(await self._delegate.read_rssi()) diff --git a/bleak/backends/corebluetooth/descriptor.py b/bleak/backends/corebluetooth/descriptor.py deleted file mode 100644 index 646a16057..000000000 --- a/bleak/backends/corebluetooth/descriptor.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Interface class for the Bleak representation of a GATT Descriptor - -Created on 2019-06-28 by kevincar - -""" - -from CoreBluetooth import CBDescriptor - -from ..corebluetooth.utils import cb_uuid_to_str -from ..descriptor import BleakGATTDescriptor - - -class BleakGATTDescriptorCoreBluetooth(BleakGATTDescriptor): - """GATT Descriptor implementation for CoreBluetooth backend""" - - def __init__( - self, obj: CBDescriptor, characteristic_uuid: str, characteristic_handle: int - ): - super(BleakGATTDescriptorCoreBluetooth, self).__init__(obj) - self.obj: CBDescriptor = obj - self.__characteristic_uuid: str = characteristic_uuid - self.__characteristic_handle: int = characteristic_handle - - @property - def characteristic_handle(self) -> int: - """handle for the characteristic that this descriptor belongs to""" - return self.__characteristic_handle - - @property - def characteristic_uuid(self) -> str: - """UUID for the characteristic that this descriptor belongs to""" - return self.__characteristic_uuid - - @property - def uuid(self) -> str: - """UUID for this descriptor""" - return cb_uuid_to_str(self.obj.UUID()) - - @property - def handle(self) -> int: - """Integer handle for this descriptor""" - return int(self.obj.handle()) diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index 3491577df..39a85da4c 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -1,31 +1,45 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "darwin": + assert False, "This backend is only available on macOS" + import logging -from typing import Any, Dict, List, Literal, Optional, TypedDict +from typing import Any, Literal, Optional +from warnings import warn + +if sys.version_info < (3, 12): + from typing_extensions import override +else: + from typing import override import objc from CoreBluetooth import CBPeripheral -from Foundation import NSBundle - -from ...exc import BleakError -from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner -from .CentralManagerDelegate import CentralManagerDelegate -from .utils import cb_uuid_to_str +from Foundation import NSBundle, NSDictionary + +from bleak.args.corebluetooth import CBScannerArgs as _CBScannerArgs +from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate +from bleak.backends.corebluetooth.utils import cb_uuid_to_str +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BaseBleakScanner, +) +from bleak.exc import BleakError logger = logging.getLogger(__name__) -class CBScannerArgs(TypedDict, total=False): - """ - Platform-specific :class:`BleakScanner` args for the CoreBluetooth backend. - """ - - use_bdaddr: bool - """ - If true, use Bluetooth address instead of UUID. - - .. warning:: This uses an undocumented IOBluetooth API to get the Bluetooth - address and may break in the future macOS releases. `It is known to not - work on macOS 10.15 `_. - """ +def __getattr__(name: str): + if name == "CBScannerArgs": + warn( + "importing CBScannerArgs from bleak.backends.corebluetooth.scanner is deprecated, use bleak.args.corebluetooth instead", + DeprecationWarning, + stacklevel=2, + ) + return _CBScannerArgs + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") class BleakScannerCoreBluetooth(BaseBleakScanner): @@ -59,11 +73,11 @@ class BleakScannerCoreBluetooth(BaseBleakScanner): def __init__( self, detection_callback: Optional[AdvertisementDataCallback], - service_uuids: Optional[List[str]], + service_uuids: Optional[list[str]], scanning_mode: Literal["active", "passive"], *, - cb: CBScannerArgs, - **kwargs + cb: _CBScannerArgs, + **kwargs: Any, ): super(BleakScannerCoreBluetooth, self).__init__( detection_callback, service_uuids @@ -74,7 +88,9 @@ def __init__( if scanning_mode == "passive": raise BleakError("macOS does not support passive scanning") - self._manager = CentralManagerDelegate.alloc().init() + manager = CentralManagerDelegate.alloc().init() + assert manager + self._manager = manager self._timeout: float = kwargs.get("timeout", 5.0) if ( objc.macos_available(12, 0) @@ -87,10 +103,11 @@ def __init__( "macOS 12.0, 12.1 and 12.2 require non-empty service_uuids kwarg, otherwise no advertisement data will be received" ) + @override async def start(self) -> None: self.seen_devices = {} - def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None: + def callback(p: CBPeripheral, a: NSDictionary, r: int) -> None: service_uuids = [ cb_uuid_to_str(u) for u in a.get("kCBAdvDataServiceUUIDs", []) @@ -107,7 +124,7 @@ def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None: # Process manufacturer data into a more friendly format manufacturer_binary_data = a.get("kCBAdvDataManufacturerData") - manufacturer_data = {} + manufacturer_data: dict[int, bytes] = {} if manufacturer_binary_data: manufacturer_id = int.from_bytes( manufacturer_binary_data[0:2], byteorder="little" @@ -130,7 +147,7 @@ def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None: if self._use_bdaddr: # HACK: retrieveAddressForPeripheral_ is undocumented but seems to do the trick - address_bytes: bytes = ( + address_bytes: Optional[bytes] = ( self._manager.central_manager.retrieveAddressForPeripheral_(p) ) if address_bytes is None: @@ -143,6 +160,7 @@ def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None: address = p.identifier().UUIDString() device = self.create_or_update_device( + p.identifier().UUIDString(), address, p.name(), (p, self._manager.central_manager.delegate()), @@ -154,32 +172,7 @@ def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None: self._manager.callbacks[id(self)] = callback await self._manager.start_scan(self._service_uuids) + @override async def stop(self) -> None: await self._manager.stop_scan() self._manager.callbacks.pop(id(self), None) - - def set_scanning_filter(self, **kwargs) -> None: - """Set scanning filter for the scanner. - - .. note:: - - This is not implemented for macOS yet. - - Raises: - - ``NotImplementedError`` - - """ - raise NotImplementedError( - "Need to evaluate which macOS versions to support first..." - ) - - # macOS specific methods - - @property - def is_scanning(self): - # TODO: Evaluate if newer macOS than 10.11 has isScanning. - try: - return self._manager.isScanning_ - except Exception: - return None diff --git a/bleak/backends/corebluetooth/service.py b/bleak/backends/corebluetooth/service.py deleted file mode 100644 index 14a35a7a9..000000000 --- a/bleak/backends/corebluetooth/service.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import List - -from CoreBluetooth import CBService - -from ..service import BleakGATTService -from .characteristic import BleakGATTCharacteristicCoreBluetooth -from .utils import cb_uuid_to_str - - -class BleakGATTServiceCoreBluetooth(BleakGATTService): - """GATT Characteristic implementation for the CoreBluetooth backend""" - - def __init__(self, obj: CBService): - super().__init__(obj) - self.__characteristics: List[BleakGATTCharacteristicCoreBluetooth] = [] - # N.B. the `startHandle` method of the CBService is an undocumented Core Bluetooth feature, - # which Bleak takes advantage of in order to have a service handle to use. - self.__handle: int = int(self.obj.startHandle()) - - @property - def handle(self) -> int: - """The integer handle of this service""" - return self.__handle - - @property - def uuid(self) -> str: - """UUID for this service.""" - return cb_uuid_to_str(self.obj.UUID()) - - @property - def characteristics(self) -> List[BleakGATTCharacteristicCoreBluetooth]: - """List of characteristics for this service""" - return self.__characteristics - - def add_characteristic( - self, characteristic: BleakGATTCharacteristicCoreBluetooth - ) -> None: - """Add a :py:class:`~BleakGATTCharacteristicCoreBluetooth` to the service. - - Should not be used by end user, but rather by `bleak` itself. - """ - self.__characteristics.append(characteristic) diff --git a/bleak/backends/corebluetooth/utils.py b/bleak/backends/corebluetooth/utils.py index 920954739..d462c4aba 100644 --- a/bleak/backends/corebluetooth/utils.py +++ b/bleak/backends/corebluetooth/utils.py @@ -1,7 +1,13 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "darwin": + assert False, "This backend is only available on macOS" + from CoreBluetooth import CBUUID -from Foundation import NSData -from ...uuids import normalize_uuid_str +from bleak.uuids import normalize_uuid_str def cb_uuid_to_str(uuid: CBUUID) -> str: @@ -17,26 +23,3 @@ def cb_uuid_to_str(uuid: CBUUID) -> str: The UUID as a lower case Python string (``xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx``) """ return normalize_uuid_str(uuid.UUIDString()) - - -def _is_uuid_16bit_compatible(_uuid: str) -> bool: - test_uuid = "0000ffff-0000-1000-8000-00805f9b34fb" - test_int = _convert_uuid_to_int(test_uuid) - uuid_int = _convert_uuid_to_int(_uuid) - result_int = uuid_int & test_int - return uuid_int == result_int - - -def _convert_uuid_to_int(_uuid: str) -> int: - UUID_cb = CBUUID.alloc().initWithString_(_uuid) - UUID_data = UUID_cb.data() - UUID_bytes = UUID_data.getBytes_length_(None, len(UUID_data)) - UUID_int = int.from_bytes(UUID_bytes, byteorder="big") - return UUID_int - - -def _convert_int_to_uuid(i: int) -> str: - UUID_bytes = i.to_bytes(length=16, byteorder="big") - UUID_data = NSData.alloc().initWithBytes_length_(UUID_bytes, len(UUID_bytes)) - UUID_cb = CBUUID.alloc().initWithData_(UUID_data) - return UUID_cb.UUIDString().lower() diff --git a/bleak/backends/descriptor.py b/bleak/backends/descriptor.py index 828ead569..6cf7fd561 100644 --- a/bleak/backends/descriptor.py +++ b/bleak/backends/descriptor.py @@ -1,14 +1,18 @@ # -*- coding: utf-8 -*- +# Created on 2019-03-19 by hbldh """ Interface class for the Bleak representation of a GATT Descriptor +""" +from __future__ import annotations -Created on 2019-03-19 by hbldh +from typing import TYPE_CHECKING, Any -""" -import abc -from typing import Any +from bleak.uuids import normalize_uuid_16 + +# avoid circular import +if TYPE_CHECKING: + from bleak.backends.characteristic import BleakGATTCharacteristic -from ..uuids import normalize_uuid_16 _descriptor_descriptions = { normalize_uuid_16(0x2905): [ @@ -104,38 +108,46 @@ } -class BleakGATTDescriptor(abc.ABC): - """Interface for the Bleak representation of a GATT Descriptor""" +class BleakGATTDescriptor: + """The Bleak representation of a GATT Descriptor""" - def __init__(self, obj: Any): + def __init__( + self, obj: Any, handle: int, uuid: str, characteristic: BleakGATTCharacteristic + ): + """ + Args: + obj: The backend-specific object for the descriptor. + handle: The handle of the descriptor. + uuid: The UUID of the descriptor. + characteristic: The characteristic that this descriptor belongs to. + """ self.obj = obj + self._handle = handle + self._uuid = uuid + self._characteristic = characteristic def __str__(self): return f"{self.uuid} (Handle: {self.handle}): {self.description}" @property - @abc.abstractmethod def characteristic_uuid(self) -> str: """UUID for the characteristic that this descriptor belongs to""" - raise NotImplementedError() + return self._characteristic.uuid @property - @abc.abstractmethod def characteristic_handle(self) -> int: """handle for the characteristic that this descriptor belongs to""" - raise NotImplementedError() + return self._characteristic.handle @property - @abc.abstractmethod def uuid(self) -> str: """UUID for this descriptor""" - raise NotImplementedError() + return self._uuid @property - @abc.abstractmethod def handle(self) -> int: """Integer handle for this descriptor""" - raise NotImplementedError() + return self._handle @property def description(self) -> str: diff --git a/bleak/backends/device.py b/bleak/backends/device.py index 5ce5c89a2..445a438fc 100644 --- a/bleak/backends/device.py +++ b/bleak/backends/device.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- +# Created on 2018-04-23 by hbldh """ Wrapper class for Bluetooth LE servers returned from calling :py:meth:`bleak.discover`. - -Created on 2018-04-23 by hbldh - """ @@ -17,11 +15,9 @@ class BLEDevice: A simple wrapper class representing a BLE server detected during scanning. """ - __slots__ = ("address", "name", "details", "_rssi", "_metadata") + __slots__ = ("address", "name", "details") - def __init__( - self, address: str, name: Optional[str], details: Any, rssi: int, **kwargs - ): + def __init__(self, address: str, name: Optional[str], details: Any, **kwargs: Any): #: The Bluetooth address of the device on this machine (UUID on macOS). self.address = address #: The operating system name of the device (not necessarily the local name @@ -30,41 +26,12 @@ def __init__( #: The OS native details required for connecting to the device. self.details = details - # for backwards compatibility - self._rssi = rssi - self._metadata = kwargs - - @property - def rssi(self) -> int: - """ - Gets the RSSI of the last received advertisement. - - .. deprecated:: 0.19.0 - Use :class:`AdvertisementData` from detection callback or - :attr:`BleakScanner.discovered_devices_and_advertisement_data` instead. - """ - warn( - "BLEDevice.rssi is deprecated and will be removed in a future version of Bleak, use AdvertisementData.rssi instead", - FutureWarning, - stacklevel=2, - ) - return self._rssi - - @property - def metadata(self) -> dict: - """ - Gets additional advertisement data for the device. - - .. deprecated:: 0.19.0 - Use :class:`AdvertisementData` from detection callback or - :attr:`BleakScanner.discovered_devices_and_advertisement_data` instead. - """ - warn( - "BLEDevice.metadata is deprecated and will be removed in a future version of Bleak, use AdvertisementData instead", - FutureWarning, - stacklevel=2, - ) - return self._metadata + if kwargs: + warn( + "Passing additional arguments for BLEDevice is deprecated and has no effect.", + DeprecationWarning, + stacklevel=2, + ) def __str__(self): return f"{self.address}: {self.name}" diff --git a/bleak/backends/p4android/characteristic.py b/bleak/backends/p4android/characteristic.py deleted file mode 100644 index d9f6f1912..000000000 --- a/bleak/backends/p4android/characteristic.py +++ /dev/null @@ -1,96 +0,0 @@ -from typing import Callable, List, Union -from uuid import UUID - -from ...exc import BleakError -from ..characteristic import BleakGATTCharacteristic -from ..descriptor import BleakGATTDescriptor -from . import defs - - -class BleakGATTCharacteristicP4Android(BleakGATTCharacteristic): - """GATT Characteristic implementation for the python-for-android backend""" - - def __init__( - self, - java, - service_uuid: str, - service_handle: int, - max_write_without_response_size: Callable[[], int], - ): - super(BleakGATTCharacteristicP4Android, self).__init__( - java, max_write_without_response_size - ) - self.__uuid = self.obj.getUuid().toString() - self.__handle = self.obj.getInstanceId() - self.__service_uuid = service_uuid - self.__service_handle = service_handle - self.__descriptors = [] - self.__notification_descriptor = None - - self.__properties = [ - name - for flag, name in defs.CHARACTERISTIC_PROPERTY_DBUS_NAMES.items() - if flag & self.obj.getProperties() - ] - - @property - def service_uuid(self) -> str: - """The uuid of the Service containing this characteristic""" - return self.__service_uuid - - @property - def service_handle(self) -> int: - """The integer handle of the Service containing this characteristic""" - return int(self.__service_handle) - - @property - def handle(self) -> int: - """The handle of this characteristic""" - return self.__handle - - @property - def uuid(self) -> str: - """The uuid of this characteristic""" - return self.__uuid - - @property - def properties(self) -> List[str]: - """Properties of this characteristic""" - return self.__properties - - @property - def descriptors(self) -> List[BleakGATTDescriptor]: - """List of descriptors for this service""" - return self.__descriptors - - def get_descriptor( - self, specifier: Union[str, UUID] - ) -> Union[BleakGATTDescriptor, None]: - """Get a descriptor by UUID (str or uuid.UUID)""" - if isinstance(specifier, int): - raise BleakError( - "The Android Bluetooth API does not provide access to descriptor handles." - ) - - matches = [ - descriptor - for descriptor in self.descriptors - if descriptor.uuid == str(specifier) - ] - if len(matches) == 0: - return None - return matches[0] - - def add_descriptor(self, descriptor: BleakGATTDescriptor): - """Add a :py:class:`~BleakGATTDescriptor` to the characteristic. - - Should not be used by end user, but rather by `bleak` itself. - """ - self.__descriptors.append(descriptor) - if descriptor.uuid == defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID: - self.__notification_descriptor = descriptor - - @property - def notification_descriptor(self) -> BleakGATTDescriptor: - """The notification descriptor. Mostly needed by `bleak`, not by end user""" - return self.__notification_descriptor diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index f1bca4dd5..efa13f5ae 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -2,24 +2,35 @@ """ BLE Client for python-for-android """ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "android": + assert False, "This backend is only available on Android" + import asyncio import logging import uuid import warnings -from typing import Optional, Set, Union +from typing import Any, Optional, Union + +if sys.version_info < (3, 12): + from typing_extensions import override +else: + from typing import override from android.broadcast import BroadcastReceiver from jnius import java_method -from ...exc import BleakCharacteristicNotFoundError, BleakError -from ..characteristic import BleakGATTCharacteristic -from ..client import BaseBleakClient, NotifyCallback -from ..device import BLEDevice -from ..service import BleakGATTServiceCollection -from . import defs, utils -from .characteristic import BleakGATTCharacteristicP4Android -from .descriptor import BleakGATTDescriptorP4Android -from .service import BleakGATTServiceP4Android +from bleak.assigned_numbers import gatt_char_props_to_strs +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, NotifyCallback +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.backends.device import BLEDevice +from bleak.backends.p4android import defs, utils +from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection +from bleak.exc import BleakError logger = logging.getLogger(__name__) @@ -38,7 +49,7 @@ class BleakClientP4Android(BaseBleakClient): def __init__( self, address_or_ble_device: Union[BLEDevice, str], - services: Optional[Set[uuid.UUID]], + services: Optional[set[uuid.UUID]], **kwargs, ): super(BleakClientP4Android, self).__init__(address_or_ble_device, **kwargs) @@ -50,20 +61,14 @@ def __init__( self.__gatt = None self.__mtu = 23 - def __del__(self): - if self.__gatt is not None: - self.__gatt.close() - self.__gatt = None - # Connectivity methods - async def connect(self, **kwargs) -> bool: - """Connect to the specified GATT server. + @override + async def connect(self, pair: bool, **kwargs) -> None: + """Connect to the specified GATT server.""" + if pair: + logger.warning("Pairing during connect is not implemented on Android") - Returns: - Boolean representing connection status. - - """ loop = asyncio.get_running_loop() self.__adapter = defs.BluetoothAdapter.getDefaultAdapter() @@ -112,7 +117,7 @@ async def connect(self, **kwargs) -> bool: resultApi="onServicesDiscovered", ) - await self.get_services() + await self._get_services() except BaseException: # if connecting is canceled or one of the above fails, we need to # disconnect @@ -122,22 +127,16 @@ async def connect(self, **kwargs) -> bool: pass raise - return True - - async def disconnect(self) -> bool: - """Disconnect from the specified GATT server. - - Returns: - Boolean representing if device is disconnected. - - """ + @override + async def disconnect(self) -> None: + """Disconnect from the specified GATT server.""" logger.debug("Disconnecting from BLE device...") if self.__gatt is None: # No connection exists. Either one hasn't been created or # we have already called disconnect and closed the gatt # connection. logger.debug("already disconnected") - return True + return # Try to disconnect the actual device/peripheral try: @@ -159,17 +158,12 @@ async def disconnect(self) -> bool: # Reset all stored services. self.services = None - return True - - async def pair(self, *args, **kwargs) -> bool: + @override + async def pair(self, *args, **kwargs) -> None: """Pair with the peripheral. You can use ConnectDevice method if you already know the MAC address of the device. Else you need to StartDiscovery, Trust, Pair and Connect in sequence. - - Returns: - Boolean regarding success of pairing. - """ loop = asyncio.get_running_loop() @@ -201,30 +195,26 @@ def handleBondStateChanged(context, intent): # See if it is already paired. bond_state = self.__device.getBondState() if bond_state == defs.BluetoothDevice.BOND_BONDED: - return True + return elif bond_state == defs.BluetoothDevice.BOND_NONE: logger.debug(f"Pairing to BLE device @ {self.address}") if not self.__device.createBond(): raise BleakError( f"Could not initiate bonding with device @ {self.address}" ) - return await bondedFuture + await bondedFuture finally: await receiver.stop() - async def unpair(self) -> bool: - """Unpair with the peripheral. - - Returns: - Boolean regarding success of unpairing. - - """ + @override + async def unpair(self) -> None: + """Unpair with the peripheral.""" warnings.warn( "Unpairing is seemingly unavailable in the Android API at the moment." ) - return False @property + @override def is_connected(self) -> bool: """Check connection status between this client and the server. @@ -239,12 +229,13 @@ def is_connected(self) -> bool: ) @property - def mtu_size(self) -> Optional[int]: + @override + def mtu_size(self) -> int: return self.__mtu # GATT services methods - async def get_services(self) -> BleakGATTServiceCollection: + async def _get_services(self) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. Returns: @@ -264,16 +255,22 @@ async def get_services(self) -> BleakGATTServiceCollection: ): continue - service = BleakGATTServiceP4Android(java_service) + service = BleakGATTService( + java_service, + java_service.getInstanceId(), + java_service.getUuid().toString(), + ) services.add_service(service) for java_characteristic in java_service.getCharacteristics(): - characteristic = BleakGATTCharacteristicP4Android( + characteristic = BleakGATTCharacteristic( java_characteristic, - service.uuid, - service.handle, + java_characteristic.getInstanceId(), + java_characteristic.getUuid().toString(), + gatt_char_props_to_strs((java_characteristic.getProperties())), lambda: self.__mtu - 3, + service, ) services.add_characteristic(characteristic) @@ -281,11 +278,11 @@ async def get_services(self) -> BleakGATTServiceCollection: java_characteristic.getDescriptors() ): - descriptor = BleakGATTDescriptorP4Android( + descriptor = BleakGATTDescriptor( java_descriptor, - characteristic.uuid, - characteristic.handle, - descriptor_index, + characteristic.handle + 1 + descriptor_index, + self.obj.getUuid().toString(), + characteristic, ) services.add_descriptor(descriptor) @@ -294,29 +291,19 @@ async def get_services(self) -> BleakGATTServiceCollection: # IO methods + @override async def read_gatt_char( - self, - char_specifier: Union[BleakGATTCharacteristicP4Android, int, str, uuid.UUID], - **kwargs, + self, characteristic: BleakGATTCharacteristic, **kwargs: Any ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to read from, - specified by either integer handle, UUID or directly by the - BleakGATTCharacteristicP4Android object representing it. + characteristic (BleakGATTCharacteristic): The characteristic to read from. Returns: (bytearray) The read data. """ - if not isinstance(char_specifier, BleakGATTCharacteristicP4Android): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - - if not characteristic: - raise BleakCharacteristicNotFoundError(char_specifier) (value,) = await self.__callbacks.perform_and_wait( dispatchApi=self.__gatt.readCharacteristic, @@ -329,30 +316,18 @@ async def read_gatt_char( ) return value + @override async def read_gatt_descriptor( - self, - desc_specifier: Union[BleakGATTDescriptorP4Android, str, uuid.UUID], - **kwargs, + self, descriptor: BleakGATTDescriptor, **kwargs: Any ) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: - desc_specifier (BleakGATTDescriptorP4Android, str or UUID): The descriptor to read from, - specified by either UUID or directly by the - BleakGATTDescriptorP4Android object representing it. + descriptor: The descriptor to read from. Returns: - (bytearray) The read data. - + The read data. """ - if not isinstance(desc_specifier, BleakGATTDescriptorP4Android): - descriptor = self.services.get_descriptor(desc_specifier) - else: - descriptor = desc_specifier - - if not descriptor: - raise BleakError(f"Descriptor with UUID {desc_specifier} was not found!") - (value,) = await self.__callbacks.perform_and_wait( dispatchApi=self.__gatt.readDescriptor, dispatchParams=(descriptor.obj,), @@ -366,11 +341,9 @@ async def read_gatt_descriptor( return value + @override async def write_gatt_char( - self, - characteristic: BleakGATTCharacteristic, - data: bytearray, - response: bool, + self, characteristic: BleakGATTCharacteristic, data: bytearray, response: bool ) -> None: if response: characteristic.obj.setWriteType( @@ -393,21 +366,22 @@ async def write_gatt_char( f"Write Characteristic {characteristic.uuid} | {characteristic.handle}: {data}" ) + @override async def write_gatt_descriptor( self, - desc_specifier: Union[BleakGATTDescriptorP4Android, str, uuid.UUID], + desc_specifier: Union[BleakGATTDescriptor, str, uuid.UUID], data: bytearray, ) -> None: """Perform a write operation on the specified GATT descriptor. Args: - desc_specifier (BleakGATTDescriptorP4Android, str or UUID): The descriptor to write + desc_specifier (BleakGATTDescriptor, str or UUID): The descriptor to write to, specified by either UUID or directly by the - BleakGATTDescriptorP4Android object representing it. + BleakGATTDescriptor object representing it. data (bytes or bytearray): The data to send. """ - if not isinstance(desc_specifier, BleakGATTDescriptorP4Android): + if not isinstance(desc_specifier, BleakGATTDescriptor): descriptor = self.services.get_descriptor(desc_specifier) else: descriptor = desc_specifier @@ -427,6 +401,7 @@ async def write_gatt_descriptor( f"Write Descriptor {descriptor.uuid} | {descriptor.handle}: {data}" ) + @override async def start_notify( self, characteristic: BleakGATTCharacteristic, @@ -450,25 +425,15 @@ async def start_notify( defs.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, ) - async def stop_notify( - self, - char_specifier: Union[BleakGATTCharacteristicP4Android, int, str, uuid.UUID], - ) -> None: + @override + async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None: """Deactivate notification/indication on a specified characteristic. Args: - char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to deactivate - notification/indication on, specified by either integer handle, UUID or - directly by the BleakGATTCharacteristicP4Android object representing it. + characteristic (BleakGATTCharacteristic): The characteristic to deactivate + notification/indication on,. """ - if not isinstance(char_specifier, BleakGATTCharacteristicP4Android): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - if not characteristic: - raise BleakCharacteristicNotFoundError(char_specifier) - await self.write_gatt_descriptor( characteristic.notification_descriptor, defs.BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE, diff --git a/bleak/backends/p4android/defs.py b/bleak/backends/p4android/defs.py index 832f92bc3..c5a408251 100644 --- a/bleak/backends/p4android/defs.py +++ b/bleak/backends/p4android/defs.py @@ -1,4 +1,9 @@ -# -*- coding: utf-8 -*- +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "android": + assert False, "This backend is only available on Android" import enum @@ -77,15 +82,4 @@ class ScanFailed(enum.IntEnum): 0x0101: "Failure", } -CHARACTERISTIC_PROPERTY_DBUS_NAMES = { - BluetoothGattCharacteristic.PROPERTY_BROADCAST: "broadcast", - BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS: "extended-properties", - BluetoothGattCharacteristic.PROPERTY_INDICATE: "indicate", - BluetoothGattCharacteristic.PROPERTY_NOTIFY: "notify", - BluetoothGattCharacteristic.PROPERTY_READ: "read", - BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE: "authenticated-signed-writes", - BluetoothGattCharacteristic.PROPERTY_WRITE: "write", - BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE: "write-without-response", -} - CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = normalize_uuid_16(0x2902) diff --git a/bleak/backends/p4android/descriptor.py b/bleak/backends/p4android/descriptor.py deleted file mode 100644 index 844316ec8..000000000 --- a/bleak/backends/p4android/descriptor.py +++ /dev/null @@ -1,37 +0,0 @@ -from ..descriptor import BleakGATTDescriptor - - -class BleakGATTDescriptorP4Android(BleakGATTDescriptor): - """GATT Descriptor implementation for python-for-android backend""" - - def __init__( - self, java, characteristic_uuid: str, characteristic_handle: int, index: int - ): - super(BleakGATTDescriptorP4Android, self).__init__(java) - self.__uuid = self.obj.getUuid().toString() - self.__characteristic_uuid = characteristic_uuid - self.__characteristic_handle = characteristic_handle - # many devices have sequential handles and this formula will mysteriously work for them - # it's possible this formula could make duplicate handles on other devices. - self.__fake_handle = self.__characteristic_handle + 1 + index - - @property - def characteristic_handle(self) -> int: - """handle for the characteristic that this descriptor belongs to""" - return self.__characteristic_handle - - @property - def characteristic_uuid(self) -> str: - """UUID for the characteristic that this descriptor belongs to""" - return self.__characteristic_uuid - - @property - def uuid(self) -> str: - """UUID for this descriptor""" - return self.__uuid - - @property - def handle(self) -> int: - """Integer handle for this descriptor""" - # 2021-01 The Android Bluetooth API does not appear to provide access to descriptor handles. - return self.__fake_handle diff --git a/bleak/backends/p4android/scanner.py b/bleak/backends/p4android/scanner.py index fb3be74ba..7476f2d4e 100644 --- a/bleak/backends/p4android/scanner.py +++ b/bleak/backends/p4android/scanner.py @@ -1,23 +1,36 @@ -# -*- coding: utf-8 -*- +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "android": + assert False, "This backend is only available on Android" import asyncio import logging -import sys import warnings -from typing import List, Literal, Optional +from typing import Literal, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout else: from asyncio import timeout as async_timeout +if sys.version_info < (3, 12): + from typing_extensions import override +else: + from typing import override + from android.broadcast import BroadcastReceiver from android.permissions import Permission, request_permissions from jnius import cast, java_method -from ...exc import BleakError -from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner -from . import defs, utils +from bleak.backends.p4android import defs, utils +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BaseBleakScanner, +) +from bleak.exc import BleakError logger = logging.getLogger(__name__) @@ -43,7 +56,7 @@ class BleakScannerP4Android(BaseBleakScanner): def __init__( self, detection_callback: Optional[AdvertisementDataCallback], - service_uuids: Optional[List[str]], + service_uuids: Optional[list[str]], scanning_mode: Literal["active", "passive"], **kwargs, ): @@ -58,9 +71,7 @@ def __init__( self.__javascanner = None self.__callback = None - def __del__(self) -> None: - self.__stop() - + @override async def start(self) -> None: if BleakScannerP4Android.__scanner is not None: raise BleakError("A BleakScanner is already scanning on this adapter.") @@ -208,7 +219,8 @@ def handleAdapterStateChanged(context, intent): return await self.start() - def __stop(self) -> None: + @override + async def stop(self) -> None: if self.__javascanner is not None: logger.debug("Stopping BTLE scan") self.__javascanner.stopScan(self.__callback.java) @@ -217,14 +229,6 @@ def __stop(self) -> None: else: logger.debug("BTLE scan already stopped") - async def stop(self) -> None: - self.__stop() - - def set_scanning_filter(self, **kwargs) -> None: - # If we do end up implementing this, this should accept List - # and ScanSettings java objects to pass to startScan(). - raise NotImplementedError("not implemented in Android backend") - def _handle_scan_result(self, result) -> None: native_device = result.getDevice() record = result.getScanRecord() @@ -263,6 +267,7 @@ def _handle_scan_result(self, result) -> None: ) device = self.create_or_update_device( + native_device.getAddress(), native_device.getAddress(), native_device.getName(), native_device, diff --git a/bleak/backends/p4android/service.py b/bleak/backends/p4android/service.py deleted file mode 100644 index aab1fc278..000000000 --- a/bleak/backends/p4android/service.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import List - -from ..service import BleakGATTService -from .characteristic import BleakGATTCharacteristicP4Android - - -class BleakGATTServiceP4Android(BleakGATTService): - """GATT Service implementation for the python-for-android backend""" - - def __init__(self, java): - super().__init__(java) - self.__uuid = self.obj.getUuid().toString() - self.__handle = self.obj.getInstanceId() - self.__characteristics = [] - - @property - def uuid(self) -> str: - """The UUID to this service""" - return self.__uuid - - @property - def handle(self) -> int: - """A unique identifier for this service""" - return self.__handle - - @property - def characteristics(self) -> List[BleakGATTCharacteristicP4Android]: - """List of characteristics for this service""" - return self.__characteristics - - def add_characteristic(self, characteristic: BleakGATTCharacteristicP4Android): - """Add a :py:class:`~BleakGATTCharacteristicP4Android` to the service. - - Should not be used by end user, but rather by `bleak` itself. - """ - self.__characteristics.append(characteristic) diff --git a/bleak/backends/p4android/utils.py b/bleak/backends/p4android/utils.py index a4fafda38..0850acda8 100644 --- a/bleak/backends/p4android/utils.py +++ b/bleak/backends/p4android/utils.py @@ -1,4 +1,9 @@ -# -*- coding: utf-8 -*- +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "android": + assert False, "This backend is only available on Android" import asyncio import logging @@ -6,7 +11,7 @@ from jnius import PythonJavaClass -from ...exc import BleakError +from bleak.exc import BleakError logger = logging.getLogger(__name__) diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index eb0d71f06..05ebfda24 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -3,25 +3,14 @@ import inspect import os import platform -from typing import ( - Any, - Callable, - Coroutine, - Dict, - Hashable, - List, - NamedTuple, - Optional, - Set, - Tuple, - Type, -) - -from ..exc import BleakError -from .device import BLEDevice +from collections.abc import Callable, Coroutine, Hashable +from typing import Any, NamedTuple, Optional + +from bleak.backends.device import BLEDevice +from bleak.exc import BleakError # prevent tasks from being garbage collected -_background_tasks: Set[asyncio.Task] = set() +_background_tasks = set[asyncio.Task[None]]() class AdvertisementData(NamedTuple): @@ -34,7 +23,7 @@ class AdvertisementData(NamedTuple): The local name of the device or ``None`` if not included in advertising data. """ - manufacturer_data: Dict[int, bytes] + manufacturer_data: dict[int, bytes] """ Dictionary of manufacturer data in bytes from the received advertisement data or empty dict if not present. @@ -43,12 +32,12 @@ class AdvertisementData(NamedTuple): https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/ """ - service_data: Dict[str, bytes] + service_data: dict[str, bytes] """ Dictionary of service data from the received advertisement data or empty dict if not present. """ - service_uuids: List[str] + service_uuids: list[str] """ List of service UUIDs from the received advertisement data or empty list if not present. """ @@ -67,7 +56,7 @@ class AdvertisementData(NamedTuple): .. versionadded:: 0.19 """ - platform_data: Tuple + platform_data: tuple[Any, ...] """ Tuple of platform specific data. @@ -75,7 +64,7 @@ class AdvertisementData(NamedTuple): """ def __repr__(self) -> str: - kwargs = [] + kwargs: list[str] = [] if self.local_name: kwargs.append(f"local_name={repr(self.local_name)}") if self.manufacturer_data: @@ -122,21 +111,23 @@ class BaseBleakScanner(abc.ABC): containing this advertising data will be received. """ - seen_devices: Dict[str, Tuple[BLEDevice, AdvertisementData]] + seen_devices: dict[str, tuple[BLEDevice, AdvertisementData]] """ Map of device identifier to BLEDevice and most recent advertisement data. + The key is a backend-specific identifier for the device. + This map must be cleared when scanning starts. """ def __init__( self, detection_callback: Optional[AdvertisementDataCallback], - service_uuids: Optional[List[str]], + service_uuids: Optional[list[str]], ): super(BaseBleakScanner, self).__init__() - self._ad_callbacks: Dict[ + self._ad_callbacks: dict[ Hashable, Callable[[BLEDevice, AdvertisementData], None] ] = {} """ @@ -146,7 +137,7 @@ def __init__( if detection_callback is not None: self.register_detection_callback(detection_callback) - self._service_uuids: Optional[List[str]] = ( + self._service_uuids: Optional[list[str]] = ( [u.lower() for u in service_uuids] if service_uuids is not None else None ) @@ -197,7 +188,7 @@ def remove() -> None: return remove - def is_allowed_uuid(self, service_uuids: Optional[List[str]]) -> bool: + def is_allowed_uuid(self, service_uuids: Optional[list[str]]) -> bool: """ Check if the advertisement data contains any of the service UUIDs matching the filter. If no filter is set, this will always return @@ -247,12 +238,18 @@ def call_detection_callbacks( callback(device, advertisement_data) def create_or_update_device( - self, address: str, name: str, details: Any, adv: AdvertisementData + self, + key: str, + address: str, + name: Optional[str], + details: Any, + adv: AdvertisementData, ) -> BLEDevice: """ Creates or updates a device in :attr:`seen_devices`. Args: + key: A backend-specific identifier for the device. address: The Bluetooth address of the device (UUID on macOS). name: The OS display name for the device. details: The platform-specific handle for the device. @@ -262,28 +259,14 @@ def create_or_update_device( The updated device. """ - # for backwards compatibility, see https://github.com/hbldh/bleak/issues/1025 - metadata = dict( - uuids=adv.service_uuids, - manufacturer_data=adv.manufacturer_data, - ) - try: - device, _ = self.seen_devices[address] + device, _ = self.seen_devices[key] device.name = name - device._rssi = adv.rssi - device._metadata = metadata except KeyError: - device = BLEDevice( - address, - name, - details, - adv.rssi, - **metadata, - ) + device = BLEDevice(address, name, details) - self.seen_devices[address] = (device, adv) + self.seen_devices[key] = (device, adv) return device @@ -297,18 +280,8 @@ async def stop(self) -> None: """Stop scanning for devices""" raise NotImplementedError() - @abc.abstractmethod - def set_scanning_filter(self, **kwargs) -> None: - """Set scanning filter for the BleakScanner. - - Args: - **kwargs: The filter details. This will differ a lot between backend implementations. - - """ - raise NotImplementedError() - -def get_platform_scanner_backend_type() -> Type[BaseBleakScanner]: +def get_platform_scanner_backend_type() -> type[BaseBleakScanner]: """ Gets the platform-specific :class:`BaseBleakScanner` type. """ diff --git a/bleak/backends/service.py b/bleak/backends/service.py index 09c503c99..3bbf56469 100644 --- a/bleak/backends/service.py +++ b/bleak/backends/service.py @@ -1,43 +1,42 @@ # -*- coding: utf-8 -*- +# Created on 2019-03-19 by hbldh """ Gatt Service Collection class and interface class for the Bleak representation of a GATT Service. - -Created on 2019-03-19 by hbldh - """ -import abc import logging -from typing import Any, Dict, Iterator, List, Optional, Union +from collections.abc import Iterator +from typing import Any, Optional, Union, cast from uuid import UUID -from ..exc import BleakError -from ..uuids import normalize_uuid_str, uuidstr_to_str -from .characteristic import BleakGATTCharacteristic -from .descriptor import BleakGATTDescriptor +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.exc import BleakError +from bleak.uuids import normalize_uuid_str, uuidstr_to_str logger = logging.getLogger(__name__) -class BleakGATTService(abc.ABC): - """Interface for the Bleak representation of a GATT Service.""" +class BleakGATTService: + """The Bleak representation of a GATT Service.""" - def __init__(self, obj: Any) -> None: + def __init__(self, obj: Any, handle: int, uuid: str) -> None: self.obj = obj + self._handle = handle + self._uuid = uuid + self._characteristics: dict[int, BleakGATTCharacteristic] = {} def __str__(self) -> str: return f"{self.uuid} (Handle: {self.handle}): {self.description}" @property - @abc.abstractmethod def handle(self) -> int: """The handle of this service""" - raise NotImplementedError() + return self._handle @property - @abc.abstractmethod def uuid(self) -> str: """The UUID to this service""" - raise NotImplementedError() + return self._uuid @property def description(self) -> str: @@ -45,18 +44,22 @@ def description(self) -> str: return uuidstr_to_str(self.uuid) @property - @abc.abstractmethod - def characteristics(self) -> List[BleakGATTCharacteristic]: + def characteristics(self) -> list[BleakGATTCharacteristic]: """List of characteristics for this service""" - raise NotImplementedError() + return list(self._characteristics.values()) - @abc.abstractmethod def add_characteristic(self, characteristic: BleakGATTCharacteristic) -> None: """Add a :py:class:`~BleakGATTCharacteristic` to the service. Should not be used by end user, but rather by `bleak` itself. """ - raise NotImplementedError() + if characteristic.handle in self._characteristics: + raise BleakError( + "The characteristic '%s' is already present in this BleakGATTService!", + characteristic.handle, + ) + + self._characteristics[characteristic.handle] = characteristic def get_characteristic( self, uuid: Union[str, UUID] @@ -73,7 +76,9 @@ def get_characteristic( uuid = normalize_uuid_str(str(uuid)) try: - return next(filter(lambda x: x.uuid == uuid, self.characteristics)) + return next( + filter(lambda x: x.uuid == uuid, self._characteristics.values()) + ) except StopIteration: return None @@ -82,9 +87,9 @@ class BleakGATTServiceCollection: """Simple data container for storing the peripheral's service complement.""" def __init__(self) -> None: - self.__services = {} - self.__characteristics = {} - self.__descriptors = {} + self.__services: dict[int, BleakGATTService] = {} + self.__characteristics: dict[int, BleakGATTCharacteristic] = {} + self.__descriptors: dict[int, BleakGATTDescriptor] = {} def __getitem__( self, item: Union[str, int, UUID] @@ -95,7 +100,7 @@ def __getitem__( return ( self.get_service(item) or self.get_characteristic(item) - or self.get_descriptor(item) + or self.get_descriptor(cast(int, item)) ) def __iter__(self) -> Iterator[BleakGATTService]: @@ -103,17 +108,17 @@ def __iter__(self) -> Iterator[BleakGATTService]: return iter(self.services.values()) @property - def services(self) -> Dict[int, BleakGATTService]: + def services(self) -> dict[int, BleakGATTService]: """Returns dictionary of handles mapping to BleakGATTService""" return self.__services @property - def characteristics(self) -> Dict[int, BleakGATTCharacteristic]: + def characteristics(self) -> dict[int, BleakGATTCharacteristic]: """Returns dictionary of handles mapping to BleakGATTCharacteristic""" return self.__characteristics @property - def descriptors(self) -> Dict[int, BleakGATTDescriptor]: + def descriptors(self) -> dict[int, BleakGATTDescriptor]: """Returns a dictionary of integer handles mapping to BleakGATTDescriptor""" return self.__descriptors diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py deleted file mode 100644 index 6a576bf6b..000000000 --- a/bleak/backends/winrt/characteristic.py +++ /dev/null @@ -1,142 +0,0 @@ -# -*- coding: utf-8 -*- -import sys -from typing import Callable, List, Union -from uuid import UUID - -if sys.version_info >= (3, 12): - from winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattCharacteristic, - GattCharacteristicProperties, - ) -else: - from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattCharacteristic, - GattCharacteristicProperties, - ) - -from ..characteristic import BleakGATTCharacteristic -from ..descriptor import BleakGATTDescriptor - -_GattCharacteristicsPropertiesMap = { - GattCharacteristicProperties.NONE: ( - "None", - "The characteristic doesn’t have any properties that apply", - ), - GattCharacteristicProperties.BROADCAST: ( - "Broadcast".lower(), - "The characteristic supports broadcasting", - ), - GattCharacteristicProperties.READ: ( - "Read".lower(), - "The characteristic is readable", - ), - GattCharacteristicProperties.WRITE_WITHOUT_RESPONSE: ( - "Write-Without-Response".lower(), - "The characteristic supports Write Without Response", - ), - GattCharacteristicProperties.WRITE: ( - "Write".lower(), - "The characteristic is writable", - ), - GattCharacteristicProperties.NOTIFY: ( - "Notify".lower(), - "The characteristic is notifiable", - ), - GattCharacteristicProperties.INDICATE: ( - "Indicate".lower(), - "The characteristic is indicatable", - ), - GattCharacteristicProperties.AUTHENTICATED_SIGNED_WRITES: ( - "Authenticated-Signed-Writes".lower(), - "The characteristic supports signed writes", - ), - GattCharacteristicProperties.EXTENDED_PROPERTIES: ( - "Extended-Properties".lower(), - "The ExtendedProperties Descriptor is present", - ), - GattCharacteristicProperties.RELIABLE_WRITES: ( - "Reliable-Writes".lower(), - "The characteristic supports reliable writes", - ), - GattCharacteristicProperties.WRITABLE_AUXILIARIES: ( - "Writable-Auxiliaries".lower(), - "The characteristic has writable auxiliaries", - ), -} - - -class BleakGATTCharacteristicWinRT(BleakGATTCharacteristic): - """GATT Characteristic implementation for the .NET backend, implemented with WinRT""" - - def __init__( - self, - obj: GattCharacteristic, - max_write_without_response_size: Callable[[], int], - ): - super().__init__(obj, max_write_without_response_size) - self.__descriptors = [] - self.__props = [ - _GattCharacteristicsPropertiesMap[v][0] - for v in [2**n for n in range(10)] - if (self.obj.characteristic_properties & v) - ] - - @property - def service_uuid(self) -> str: - """The uuid of the Service containing this characteristic""" - return str(self.obj.service.uuid) - - @property - def service_handle(self) -> int: - """The integer handle of the Service containing this characteristic""" - return int(self.obj.service.attribute_handle) - - @property - def handle(self) -> int: - """The handle of this characteristic""" - return int(self.obj.attribute_handle) - - @property - def uuid(self) -> str: - """The uuid of this characteristic""" - return str(self.obj.uuid) - - @property - def description(self) -> str: - """Description for this characteristic""" - return ( - self.obj.user_description - if self.obj.user_description - else super().description - ) - - @property - def properties(self) -> List[str]: - """Properties of this characteristic""" - return self.__props - - @property - def descriptors(self) -> List[BleakGATTDescriptor]: - """List of descriptors for this characteristic""" - return self.__descriptors - - def get_descriptor( - self, specifier: Union[int, str, UUID] - ) -> Union[BleakGATTDescriptor, None]: - """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" - try: - if isinstance(specifier, int): - return next(filter(lambda x: x.handle == specifier, self.descriptors)) - else: - return next( - filter(lambda x: x.uuid == str(specifier), self.descriptors) - ) - except StopIteration: - return None - - def add_descriptor(self, descriptor: BleakGATTDescriptor): - """Add a :py:class:`~BleakGATTDescriptor` to the characteristic. - - Should not be used by end user, but rather by `bleak` itself. - """ - self.__descriptors.append(descriptor) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index a04ec7c59..a604cfc0c 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -1,127 +1,103 @@ # -*- coding: utf-8 -*- +# Created on 2020-08-19 by hbldh """ BLE Client for Windows 10 systems, implemented with WinRT. - -Created on 2020-08-19 by hbldh """ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "win32": + assert False, "This backend is only available on Windows" import asyncio import logging -import sys import uuid -import warnings +from collections.abc import Callable +from contextvars import Context from ctypes import WinError -from typing import ( - Any, - Dict, - List, - Literal, - Optional, - Protocol, - Sequence, - Set, - TypedDict, - Union, - cast, -) +from typing import Any, Generic, Optional, Protocol, Sequence, TypeVar, Union, cast +from warnings import warn if sys.version_info < (3, 12): - from typing_extensions import Buffer + from typing_extensions import Buffer, override else: from collections.abc import Buffer + from typing import override if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout + from typing_extensions import Self, assert_never else: from asyncio import timeout as async_timeout - -if sys.version_info >= (3, 12): - from winrt.windows.devices.bluetooth import ( - BluetoothAddressType, - BluetoothCacheMode, - BluetoothError, - BluetoothLEDevice, - ) - from winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattCharacteristic, - GattCharacteristicProperties, - GattClientCharacteristicConfigurationDescriptorValue, - GattCommunicationStatus, - GattDescriptor, - GattDeviceService, - GattSession, - GattSessionStatus, - GattSessionStatusChangedEventArgs, - GattValueChangedEventArgs, - GattWriteOption, - ) - from winrt.windows.devices.enumeration import ( - DeviceInformation, - DevicePairingKinds, - DevicePairingResultStatus, - DeviceUnpairingResultStatus, - ) - from winrt.windows.foundation import ( - AsyncStatus, - EventRegistrationToken, - IAsyncOperation, - ) - from winrt.windows.storage.streams import Buffer as WinBuffer -else: - from bleak_winrt.windows.devices.bluetooth import ( - BluetoothAddressType, - BluetoothCacheMode, - BluetoothError, - BluetoothLEDevice, - ) - from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattCharacteristic, - GattCharacteristicProperties, - GattClientCharacteristicConfigurationDescriptorValue, - GattCommunicationStatus, - GattDescriptor, - GattDeviceService, - GattSession, - GattSessionStatus, - GattSessionStatusChangedEventArgs, - GattValueChangedEventArgs, - GattWriteOption, - ) - from bleak_winrt.windows.devices.enumeration import ( - DeviceInformation, - DevicePairingKinds, - DevicePairingResultStatus, - DeviceUnpairingResultStatus, - ) - from bleak_winrt.windows.foundation import ( - AsyncStatus, - EventRegistrationToken, - IAsyncOperation, - ) - from bleak_winrt.windows.storage.streams import Buffer as WinBuffer - -from ... import BleakScanner -from ...exc import ( - PROTOCOL_ERROR_CODES, - BleakCharacteristicNotFoundError, - BleakDeviceNotFoundError, - BleakError, + from typing import Self, assert_never + +from winrt.system import Object +from winrt.windows.devices.bluetooth import ( + BluetoothAddressType, + BluetoothCacheMode, + BluetoothError, + BluetoothLEDevice, +) +from winrt.windows.devices.bluetooth.genericattributeprofile import ( + GattCharacteristic, + GattCharacteristicProperties, + GattClientCharacteristicConfigurationDescriptorValue, + GattCommunicationStatus, + GattDescriptor, + GattDeviceService, + GattSession, + GattSessionStatus, + GattSessionStatusChangedEventArgs, + GattValueChangedEventArgs, + GattWriteOption, +) +from winrt.windows.devices.enumeration import ( + DeviceInformation, + DeviceInformationCustomPairing, + DevicePairingKinds, + DevicePairingProtectionLevel, + DevicePairingRequestedEventArgs, + DevicePairingResultStatus, + DeviceUnpairingResultStatus, +) +from winrt.windows.foundation import ( + AsyncStatus, + EventRegistrationToken, + IAsyncOperation, ) -from ..characteristic import BleakGATTCharacteristic -from ..client import BaseBleakClient, NotifyCallback -from ..device import BLEDevice -from ..service import BleakGATTServiceCollection -from .characteristic import BleakGATTCharacteristicWinRT -from .descriptor import BleakGATTDescriptorWinRT -from .scanner import BleakScannerWinRT -from .service import BleakGATTServiceWinRT +from winrt.windows.storage.streams import Buffer as WinBuffer + +from bleak import BleakScanner +from bleak.args.winrt import WinRTClientArgs as _WinRTClientArgs +from bleak.assigned_numbers import gatt_char_props_to_strs +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, NotifyCallback +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.backends.device import BLEDevice +from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection +from bleak.backends.winrt.scanner import BleakScannerWinRT, RawAdvData +from bleak.exc import PROTOCOL_ERROR_CODES, BleakDeviceNotFoundError, BleakError logger = logging.getLogger(__name__) +def __getattr__(name: str): + if name == "WinRTClientArgs": + warn( + "importing WinRTClientArgs from bleak.backends.winrt.client is deprecated, use bleak.args.winrt instead", + DeprecationWarning, + stacklevel=2, + ) + return _WinRTClientArgs + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + class _Result(Protocol): - status: GattCommunicationStatus - protocol_error: int + @property + def status(self) -> GattCommunicationStatus: ... + @property + def protocol_error(self) -> Optional[int]: ... def _address_to_int(address: str) -> int: @@ -156,6 +132,7 @@ def _ensure_success(result: _Result, attr: Optional[str], fail_msg: str) -> Any: return None if attr is None else getattr(result, attr) if status == GattCommunicationStatus.PROTOCOL_ERROR: + assert result.protocol_error is not None err = PROTOCOL_ERROR_CODES.get(result.protocol_error, "Unknown") raise BleakError( f"{fail_msg}: Protocol Error 0x{result.protocol_error:02X}: {err}" @@ -170,31 +147,6 @@ def _ensure_success(result: _Result, attr: Optional[str], fail_msg: str) -> Any: raise BleakError(f"{fail_msg}: Unexpected status code 0x{status:02X}") -class WinRTClientArgs(TypedDict, total=False): - """ - Windows-specific arguments for :class:`BleakClient`. - """ - - address_type: Literal["public", "random"] - """ - Can either be ``"public"`` or ``"random"``, depending on the required address - type needed to connect to your device. - """ - - use_cached_services: bool - """ - ``True`` allows Windows to fetch the services, characteristics and descriptors - from the Windows cache instead of reading them from the device. Can be very - much faster for known, unchanging devices, but not recommended for DIY peripherals - where the GATT layout can change between connections. - - ``False`` will force the attribute database to be read from the remote device - instead of using the OS cache. - - If omitted, the OS Bluetooth stack will do what it thinks is best. - """ - - class BleakClientWinRT(BaseBleakClient): """Native Windows Bleak Client. @@ -209,61 +161,59 @@ class BleakClientWinRT(BaseBleakClient): def __init__( self, address_or_ble_device: Union[BLEDevice, str], - services: Optional[Set[str]] = None, + services: Optional[set[str]] = None, *, - winrt: WinRTClientArgs, - **kwargs, + winrt: _WinRTClientArgs, + **kwargs: Any, ): super(BleakClientWinRT, self).__init__(address_or_ble_device, **kwargs) # Backend specific. WinRT objects. if isinstance(address_or_ble_device, BLEDevice): - data = address_or_ble_device.details - self._device_info = (data.adv or data.scan).bluetooth_address + data: RawAdvData = address_or_ble_device.details + args = data.adv or data.scan + assert args + self._device_info = args.bluetooth_address else: self._device_info = None + self._requested_services = ( [uuid.UUID(s) for s in services] if services else None ) self._requester: Optional[BluetoothLEDevice] = None - self._services_changed_events: List[asyncio.Event] = [] - self._session_active_events: List[asyncio.Event] = [] - self._session_closed_events: List[asyncio.Event] = [] - self._session: GattSession = None - self._notification_callbacks: Dict[int, NotifyCallback] = {} - - if "address_type" in kwargs: - warnings.warn( - "The address_type keyword arg will in a future version be moved into the win dict input instead.", - PendingDeprecationWarning, - stacklevel=2, - ) + self._services_changed_events: list[asyncio.Event] = [] + self._session_active_events: list[asyncio.Event] = [] + self._session: Optional[GattSession] = None + self._notification_callbacks: dict[int, EventRegistrationToken] = {} # os-specific options self._use_cached_services = winrt.get("use_cached_services") - self._address_type = winrt.get("address_type", kwargs.get("address_type")) + self._address_type = winrt.get("address_type") self._retry_on_services_changed = False self._session_services_changed_token: Optional[EventRegistrationToken] = None self._session_status_changed_token: Optional[EventRegistrationToken] = None self._max_pdu_size_changed_token: Optional[EventRegistrationToken] = None - def __str__(self): + def __str__(self) -> str: return f"{type(self).__name__} ({self.address})" # Connectivity methods async def _create_requester(self, bluetooth_address: int) -> BluetoothLEDevice: - args = [ - bluetooth_address, - ] if self._address_type is not None: - args.append( - BluetoothAddressType.PUBLIC - if self._address_type == "public" - else BluetoothAddressType.RANDOM + requester = await BluetoothLEDevice.from_bluetooth_address_with_bluetooth_address_type_async( + bluetooth_address, + ( + BluetoothAddressType.PUBLIC + if self._address_type == "public" + else BluetoothAddressType.RANDOM + ), + ) + else: + requester = await BluetoothLEDevice.from_bluetooth_address_async( + bluetooth_address ) - requester = await BluetoothLEDevice.from_bluetooth_address_async(*args) # https://github.com/microsoft/Windows-universal-samples/issues/1089#issuecomment-487586755 if requester is None: @@ -272,15 +222,12 @@ async def _create_requester(self, bluetooth_address: int) -> BluetoothLEDevice: ) return requester - async def connect(self, **kwargs) -> bool: + @override + async def connect(self, pair: bool, **kwargs: Any) -> None: """Connect to the specified GATT server. Keyword Args: timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. - - Returns: - Boolean representing connection status. - """ # Try to find the desired device. timeout = kwargs.get("timeout", self._timeout) @@ -294,8 +241,10 @@ async def connect(self, **kwargs) -> bool: self.address, f"Device with address {self.address} was not found." ) - data = device.details - self._device_info = (data.adv or data.scan).bluetooth_address + data: RawAdvData = device.details + args = data.adv or data.scan + assert args + self._device_info = args.bluetooth_address logger.debug("Connecting to BLE device @ %s", self.address) @@ -303,14 +252,17 @@ async def connect(self, **kwargs) -> bool: self._requester = await self._create_requester(self._device_info) - def handle_services_changed(): + if pair: + await self.pair(**kwargs) + + def handle_services_changed() -> None: if not self._services_changed_events: logger.warning("%s: unhandled services changed event", self.address) else: for event in self._services_changed_events: event.set() - def services_changed_handler(sender, args): + def services_changed_handler(sender: BluetoothLEDevice, args: Object) -> None: logger.debug("%s: services changed", self.address) loop.call_soon_threadsafe(handle_services_changed) @@ -319,7 +271,7 @@ def services_changed_handler(sender, args): ) # Called on disconnect event or on failure to connect. - def handle_disconnect(): + def handle_disconnect() -> None: if self._requester: if self._services_changed_token: self._requester.remove_gatt_services_changed( @@ -352,7 +304,7 @@ def handle_disconnect(): def handle_session_status_changed( args: GattSessionStatusChangedEventArgs, - ): + ) -> None: if args.error != BluetoothError.SUCCESS: logger.error("Unhandled GATT error %r", args.error) @@ -366,9 +318,6 @@ def handle_session_status_changed( if self._disconnected_callback: self._disconnected_callback() - for e in self._session_closed_events: - e.set() - handle_disconnect() # this is the WinRT event handler will be called on another thread @@ -383,7 +332,7 @@ def session_status_changed_event_handler( ) loop.call_soon_threadsafe(handle_session_status_changed, args) - def max_pdu_size_changed_handler(sender: GattSession, args): + def max_pdu_size_changed_handler(sender: GattSession, args: Object) -> None: try: max_pdu_size = sender.max_pdu_size except OSError: @@ -411,6 +360,13 @@ def max_pdu_size_changed_handler(sender: GattSession, args): ) ) + # If the session is already active, we need to set the event since + # the session_status_changed event won't fire. This happens, e.g., + # when pairing before connecting which causes the device to already + # be connected. + if self._session.session_status == GattSessionStatus.ACTIVE: + event.set() + self._max_pdu_size_changed_token = self._session.add_max_pdu_size_changed( max_pdu_size_changed_handler ) @@ -449,7 +405,7 @@ def max_pdu_size_changed_handler(sender: GattSession, args): ) get_services_task = asyncio.create_task( - self.get_services( + self._get_services( service_cache_mode=service_cache_mode, cache_mode=cache_mode, ) @@ -484,15 +440,16 @@ def max_pdu_size_changed_handler(sender: GattSession, args): except asyncio.CancelledError: pass else: - self.services = await self.get_services( + self.services = await self._get_services( service_cache_mode=service_cache_mode, cache_mode=cache_mode, ) - # 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 - await event.wait() + # 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 + await event.wait() + is_connect_complete = True finally: self._services_changed_events.remove(services_changed_event) @@ -503,20 +460,19 @@ def max_pdu_size_changed_handler(sender: GattSession, args): finally: self._session_active_events.remove(event) - return True - - async def disconnect(self) -> bool: - """Disconnect from the specified GATT server. + @override + async def disconnect(self) -> None: + """Disconnect from the specified GATT server.""" + logger.debug("Disconnecting from BLE device...") - Returns: - Boolean representing if device is disconnected. + assert self.services - """ - logger.debug("Disconnecting from BLE device...") # Remove notifications. for handle, event_handler_token in list(self._notification_callbacks.items()): char = self.services.get_characteristic(handle) - char.obj.remove_value_changed(event_handler_token) + assert char + gatt_char = cast(GattCharacteristic, char.obj) + gatt_char.remove_value_changed(event_handler_token) self._notification_callbacks.clear() # Dispose all service components that we have requested and created. @@ -530,30 +486,16 @@ async def disconnect(self) -> bool: service.obj.close() self.services = None - # Without this, disposing the BluetoothLEDevice won't disconnect it if self._session: - self._session.maintain_connection = False - # calling self._session.close() here prevents any further GATT - # session status events, so we defer that until after the session - # is no longer active + self._session.close() + self._session = None - # Dispose of the BluetoothLEDevice and see that the session - # status is now closed. if self._requester: - event = asyncio.Event() - self._session_closed_events.append(event) - try: - self._requester.close() - # sometimes it can take over one minute before Windows decides - # to end the GATT session/disconnect the device - async with async_timeout(120): - await event.wait() - finally: - self._session_closed_events.remove(event) - - return True + self._requester.close() + self._requester = None @property + @override def is_connected(self) -> bool: """Check connection status between this client and the server. @@ -561,18 +503,23 @@ def is_connected(self) -> bool: Boolean representing connection status. """ - return self._DeprecatedIsConnectedReturn( + return ( False if self._session is None else self._session.session_status == GattSessionStatus.ACTIVE ) @property + @override def mtu_size(self) -> int: """Get ATT MTU size for active connection""" return self._session.max_pdu_size - async def pair(self, protection_level: int = None, **kwargs) -> bool: + @override + async def pair( + self, + **kwargs: Any, + ) -> None: """Attempts to pair with the device. Keyword Args: @@ -581,63 +528,102 @@ async def pair(self, protection_level: int = None, **kwargs) -> bool: 1. None - Pair the device using no levels of protection. 2. Encryption - Pair the device using encryption. 3. EncryptionAndAuthentication - Pair the device using - encryption and authentication. (This will not work in Bleak...) - - Returns: - Boolean regarding success of pairing. + encryption and authentication. + .. versionchanged:: 1.0 + Issues :class:`DeprecationWarning` if used. The default + behavior has changed and this argument should no longer + be needed. """ + assert self._requester + # New local device information object created since the object from the requester isn't updated device_information = await DeviceInformation.create_from_id_async( self._requester.device_information.id ) - if ( - device_information.pairing.can_pair - and not device_information.pairing.is_paired + + if device_information.pairing.is_paired: + logging.debug("Device is already paired. Skipping pairing.") + return + + if not device_information.pairing.can_pair: + raise BleakError("Device does not support pairing") + + protection_level = kwargs.get("protection_level") + + # Currently only supporting Just Works solutions... + ceremony = DevicePairingKinds.CONFIRM_ONLY + custom_pairing = device_information.pairing.custom + + def handler( + sender: DeviceInformationCustomPairing, + args: DevicePairingRequestedEventArgs, ): - # Currently only supporting Just Works solutions... - ceremony = DevicePairingKinds.CONFIRM_ONLY - custom_pairing = device_information.pairing.custom + args.accept() - def handler(sender, args): - args.accept() + pairing_requested_token = custom_pairing.add_pairing_requested(handler) - pairing_requested_token = custom_pairing.add_pairing_requested(handler) - try: - if protection_level: - pairing_result = await custom_pairing.pair_async( - ceremony, protection_level + try: + if protection_level is not None: + warn( + "protection_level is deprecated and will be removed in a future version. The default protection level has changed, so it should be safe to omit this argument.", + DeprecationWarning, + 2, + ) + pairing_result = await custom_pairing.pair_with_protection_level_async( + ceremony, protection_level + ) + else: + for level in ( + DevicePairingProtectionLevel.ENCRYPTION_AND_AUTHENTICATION, + DevicePairingProtectionLevel.ENCRYPTION, + ): + pairing_result = ( + await custom_pairing.pair_with_protection_level_async( + ceremony, level + ) ) + if ( + pairing_result.status + != DevicePairingResultStatus.PROTECTION_LEVEL_COULD_NOT_BE_MET + ): + break + + logger.debug("Protection level %r not met. Retrying.", level) else: pairing_result = await custom_pairing.pair_async(ceremony) - except Exception as e: - raise BleakError("Failure trying to pair with device!") from e - finally: - custom_pairing.remove_pairing_requested(pairing_requested_token) + except Exception as e: + raise BleakError("Failure trying to pair with device!") from e + finally: + custom_pairing.remove_pairing_requested(pairing_requested_token) - if pairing_result.status not in ( - DevicePairingResultStatus.PAIRED, - DevicePairingResultStatus.ALREADY_PAIRED, - ): - raise BleakError(f"Could not pair with device: {pairing_result.status}") - else: - logger.info( - "Paired to device with protection level %r.", - pairing_result.protection_level_used, - ) - return True - else: - return device_information.pairing.is_paired + if pairing_result.status not in ( + DevicePairingResultStatus.PAIRED, + DevicePairingResultStatus.ALREADY_PAIRED, + ): + raise BleakError( + f"Could not pair with device: {pairing_result.status.name}" + ) - async def unpair(self) -> bool: - """Attempts to unpair from the device. + if logger.isEnabledFor(logging.DEBUG): + # pairing_result.protection_level_used doesn't seem to return + # accurate information if we don't update the DeviceInformation + # first. + device_information = await DeviceInformation.create_from_id_async( + self._requester.device_information.id + ) - N.B. unpairing also leads to disconnection in the Windows backend. + logger.debug( + "Paired to device with protection level %s.", + pairing_result.protection_level_used.name, + ) - Returns: - Boolean on whether the unparing was successful. + @override + async def unpair(self) -> None: + """Attempts to unpair from the device. + N.B. unpairing also leads to disconnection in the Windows backend. """ device = await self._create_requester( self._device_info @@ -658,16 +644,14 @@ async def unpair(self) -> bool: finally: device.close() - return True - # GATT services methods - async def get_services( + async def _get_services( self, *, service_cache_mode: Optional[BluetoothCacheMode] = None, cache_mode: Optional[BluetoothCacheMode] = None, - **kwargs, + **kwargs: Any, ) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. @@ -687,37 +671,22 @@ async def get_services( ) new_services = BleakGATTServiceCollection() - - # Each of the get_serv/char/desc_async() methods has two forms, one - # with no args and one with a cache_mode argument - srv_args = [] - args = [] - - # If the os-specific use_cached_services arg was given when BleakClient - # was created, the we use the second form with explicit cache mode. - # Otherwise we use the first form with no explicit cache mode which - # allows the OS Bluetooth stack to decide what is best. - - if service_cache_mode is not None: - srv_args.append(service_cache_mode) - - if cache_mode is not None: - args.append(cache_mode) - - def dispose_on_cancel(future): - if future._cancel_requested and future._result is not None: - logger.debug("disposing services object because of cancel") - for service in future._result: - service.close() - services: Sequence[GattDeviceService] + assert self._requester + if self._requested_services is None: - future = FutureLike(self._requester.get_gatt_services_async(*srv_args)) - future.add_done_callback(dispose_on_cancel) + if service_cache_mode is not None: + result = await FutureLike( + self._requester.get_gatt_services_with_cache_mode_async( + service_cache_mode + ) + ) + else: + result = await FutureLike(self._requester.get_gatt_services_async()) services = _ensure_success( - await FutureLike(self._requester.get_gatt_services_async(*srv_args)), + result, "services", "Could not get GATT services", ) @@ -726,13 +695,20 @@ def dispose_on_cancel(future): # REVISIT: should properly dispose services on cancel or protect from cancellation for s in self._requested_services: + if service_cache_mode is not None: + result = await FutureLike( + self._requester.get_gatt_services_for_uuid_with_cache_mode_async( + s, service_cache_mode + ) + ) + else: + result = await FutureLike( + self._requester.get_gatt_services_for_uuid_async(s) + ) + services.extend( _ensure_success( - await FutureLike( - self._requester.get_gatt_services_for_uuid_async( - s, *srv_args - ) - ), + result, "services", "Could not get GATT services", ) @@ -740,7 +716,12 @@ def dispose_on_cancel(future): try: for service in services: - result = await FutureLike(service.get_characteristics_async(*args)) + if cache_mode is not None: + result = await FutureLike( + service.get_characteristics_with_cache_mode_async(cache_mode) + ) + else: + result = await FutureLike(service.get_characteristics_async()) if result.status == GattCommunicationStatus.ACCESS_DENIED: # Windows does not allow access to services "owned" by the @@ -756,29 +737,52 @@ def dispose_on_cancel(future): f"Could not get GATT characteristics for service {service.uuid} ({service.attribute_handle})", ) - new_services.add_service(BleakGATTServiceWinRT(service)) + serv = BleakGATTService( + service, service.attribute_handle, str(service.uuid) + ) + new_services.add_service(serv) for characteristic in characteristics: + if cache_mode is not None: + result = await FutureLike( + characteristic.get_descriptors_with_cache_mode_async( + cache_mode + ) + ) + else: + result = await FutureLike( + characteristic.get_descriptors_async() + ) + descriptors: Sequence[GattDescriptor] = _ensure_success( - await FutureLike(characteristic.get_descriptors_async(*args)), + result, "descriptors", f"Could not get GATT descriptors for characteristic {characteristic.uuid} ({characteristic.attribute_handle})", ) - new_services.add_characteristic( - BleakGATTCharacteristicWinRT( - characteristic, lambda: self._session.max_pdu_size - 3 - ) + char = BleakGATTCharacteristic( + characteristic, + characteristic.attribute_handle, + str(characteristic.uuid), + list( + gatt_char_props_to_strs( + characteristic.characteristic_properties + ) + ), + lambda: self._session.max_pdu_size - 3, + serv, ) + new_services.add_characteristic(char) + for descriptor in descriptors: - new_services.add_descriptor( - BleakGATTDescriptorWinRT( - descriptor, - str(characteristic.uuid), - characteristic.attribute_handle, - ) + desc = BleakGATTDescriptor( + descriptor, + descriptor.attribute_handle, + str(descriptor.uuid), + char, ) + new_services.add_descriptor(desc) return new_services except BaseException: @@ -797,17 +801,14 @@ def dispose_on_cancel(future): # I/O methods + @override async def read_gatt_char( - self, - char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - **kwargs, + self, characteristic: BleakGATTCharacteristic, **kwargs: Any ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from, - specified by either integer handle, UUID or directly by the - BleakGATTCharacteristic object representing it. + characteristic (BleakGATTCharacteristic): The characteristic to read from. Keyword Args: use_cached (bool): ``False`` forces Windows to read the value from the @@ -820,18 +821,15 @@ async def read_gatt_char( if not self.is_connected: raise BleakError("Not connected") + assert self.services + use_cached = kwargs.get("use_cached", False) - if not isinstance(char_specifier, BleakGATTCharacteristic): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - if not characteristic: - raise BleakCharacteristicNotFoundError(char_specifier) + gatt_char = cast(GattCharacteristic, characteristic.obj) value = bytearray( _ensure_success( - await characteristic.obj.read_value_async( + await gatt_char.read_value_with_cache_mode_async( BluetoothCacheMode.CACHED if use_cached else BluetoothCacheMode.UNCACHED @@ -845,101 +843,113 @@ async def read_gatt_char( return value - async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: + @override + async def read_gatt_descriptor( + self, descriptor: BleakGATTDescriptor, **kwargs: Any + ) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: - handle (int): The handle of the descriptor to read from. + descriptor: The descriptor to read from. Keyword Args: use_cached (bool): `False` forces Windows to read the value from the device again and not use its own cached value. Defaults to `False`. Returns: - (bytearray) The read data. + The read data. """ if not self.is_connected: raise BleakError("Not connected") - use_cached = kwargs.get("use_cached", False) + assert self.services - descriptor = self.services.get_descriptor(handle) - if not descriptor: - raise BleakError(f"Descriptor with handle {handle} was not found!") + use_cached = kwargs.get("use_cached", False) + gatt_desc = cast(GattDescriptor, descriptor.obj) value = bytearray( _ensure_success( - await descriptor.obj.read_value_async( + await gatt_desc.read_value_with_cache_mode_async( BluetoothCacheMode.CACHED if use_cached else BluetoothCacheMode.UNCACHED ), "value", - f"Could not read Descriptor value for {handle:04X}", + f"Could not read Descriptor value for {descriptor.handle:04X}", ) ) - logger.debug("Read Descriptor %04X : %s", handle, value) + logger.debug("Read Descriptor %04X : %s", descriptor.handle, value) return value + @override async def write_gatt_char( - self, - characteristic: BleakGATTCharacteristic, - data: Buffer, - response: bool, + self, characteristic: BleakGATTCharacteristic, data: Buffer, response: bool ) -> None: if not self.is_connected: raise BleakError("Not connected") - response = ( - GattWriteOption.WRITE_WITH_RESPONSE - if response - else GattWriteOption.WRITE_WITHOUT_RESPONSE - ) buf = WinBuffer(len(data)) buf.length = buf.capacity + with memoryview(buf) as mv: mv[:] = data + + gatt_char = cast(GattCharacteristic, characteristic.obj) + _ensure_success( - await characteristic.obj.write_value_with_result_async(buf, response), + await gatt_char.write_value_with_result_and_option_async( + buf, + ( + GattWriteOption.WRITE_WITH_RESPONSE + if response + else GattWriteOption.WRITE_WITHOUT_RESPONSE + ), + ), None, f"Could not write value {data} to characteristic {characteristic.handle:04X}", ) - async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: + @override + async def write_gatt_descriptor( + self, descriptor: BleakGATTDescriptor, data: Buffer + ) -> None: """Perform a write operation on the specified GATT descriptor. Args: - handle: The handle of the descriptor to read from. + descriptor: The descriptor to read from. data: The data to send (any bytes-like object). """ if not self.is_connected: raise BleakError("Not connected") - descriptor = self.services.get_descriptor(handle) - if not descriptor: - raise BleakError(f"Descriptor with handle {handle} was not found!") + assert self.services buf = WinBuffer(len(data)) buf.length = buf.capacity + with memoryview(buf) as mv: mv[:] = data + + gatt_desc = cast(GattDescriptor, descriptor.obj) + _ensure_success( - await descriptor.obj.write_value_with_result_async(buf), + await gatt_desc.write_value_with_result_async(buf), None, - f"Could not write value {data!r} to descriptor {handle:04X}", + f"Could not write value {data!r} to descriptor {descriptor.handle:04X}", ) - logger.debug("Write Descriptor %04X : %s", handle, data) + logger.debug("Write Descriptor %04X : %s", descriptor.handle, data) + @override async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, - **kwargs, + **kwargs: Any, ) -> None: """ Activate notifications/indications on a characteristic. @@ -969,16 +979,16 @@ async def start_notify( def handle_value_changed( sender: GattCharacteristic, args: GattValueChangedEventArgs - ): + ) -> None: value = bytearray(args.characteristic_value) - return loop.call_soon_threadsafe(callback, value) + loop.call_soon_threadsafe(callback, value) event_handler_token = winrt_char.add_value_changed(handle_value_changed) self._notification_callbacks[characteristic.handle] = event_handler_token try: _ensure_success( - await winrt_char.write_client_characteristic_configuration_descriptor_async( + await winrt_char.write_client_characteristic_configuration_descriptor_with_result_async( cccd ), None, @@ -995,29 +1005,23 @@ def handle_value_changed( raise - async def stop_notify( - self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID] - ) -> None: + @override + async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None: """Deactivate notification/indication on a specified characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate - notification/indication on, specified by either integer handle, UUID or - directly by the BleakGATTCharacteristic object representing it. - + characteristic (BleakGATTCharacteristic): The characteristic to deactivate + notification/indication on. """ if not self.is_connected: raise BleakError("Not connected") - if not isinstance(char_specifier, BleakGATTCharacteristic): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - if not characteristic: - raise BleakCharacteristicNotFoundError(char_specifier) + assert self.services + + gatt_char = cast(GattCharacteristic, characteristic.obj) _ensure_success( - await characteristic.obj.write_client_characteristic_configuration_descriptor_async( + await gatt_char.write_client_characteristic_configuration_descriptor_with_result_async( GattClientCharacteristicConfigurationDescriptorValue.NONE ), None, @@ -1025,10 +1029,13 @@ async def stop_notify( ) event_handler_token = self._notification_callbacks.pop(characteristic.handle) - characteristic.obj.remove_value_changed(event_handler_token) + gatt_char.remove_value_changed(event_handler_token) + + +T = TypeVar("T") -class FutureLike: +class FutureLike(Generic[T]): """ Wraps a WinRT IAsyncOperation in a "future-like" object so that it can be passed to Python APIs. @@ -1038,18 +1045,20 @@ class FutureLike: _asyncio_future_blocking = False - def __init__(self, op: IAsyncOperation) -> None: + def __init__(self: Self, op: IAsyncOperation[T]) -> None: self._op = op - self._callbacks = [] + self._callbacks: list[Callable[[Self], None]] = [] self._loop = asyncio.get_running_loop() self._cancel_requested = False self._result = None - def call_callbacks(): + def call_callbacks() -> None: for c in self._callbacks: c(self) - def call_callbacks_threadsafe(op: IAsyncOperation, status: AsyncStatus): + def call_callbacks_threadsafe( + op: IAsyncOperation[T], status: AsyncStatus + ) -> None: if status == AsyncStatus.COMPLETED: # have to get result on this thread, otherwise it may not return correct value self._result = op.get_results() @@ -1058,7 +1067,7 @@ def call_callbacks_threadsafe(op: IAsyncOperation, status: AsyncStatus): op.completed = call_callbacks_threadsafe - def result(self) -> Any: + def result(self) -> T: if self._op.status == AsyncStatus.STARTED: raise asyncio.InvalidStateError @@ -1066,6 +1075,8 @@ def result(self) -> Any: if self._cancel_requested: raise asyncio.CancelledError + assert self._result + return self._result if self._op.status == AsyncStatus.CANCELED: @@ -1078,19 +1089,26 @@ def result(self) -> Any: error_code = self._op.error_code.value raise WinError(error_code) + assert_never(self._op.status) + def done(self) -> bool: return self._op.status != AsyncStatus.STARTED def cancelled(self) -> bool: return self._cancel_requested or self._op.status == AsyncStatus.CANCELED - def add_done_callback(self, callback, *, context=None) -> None: + def add_done_callback( + self, + callback: Callable[[Self], None], + *, + context: Optional[Context] = None, + ) -> None: self._callbacks.append(callback) - def remove_done_callback(self, callback) -> None: + def remove_done_callback(self, callback: Callable[[Self], None]) -> None: self._callbacks.remove(callback) - def cancel(self, msg=None) -> bool: + def cancel(self, msg: Optional[str] = None) -> bool: if self._cancel_requested or self._op.status != AsyncStatus.STARTED: return False @@ -1120,6 +1138,8 @@ def exception(self) -> Optional[Exception]: return WinError(error_code) + assert_never(self._op.status) + def get_loop(self) -> asyncio.AbstractEventLoop: return self._loop diff --git a/bleak/backends/winrt/descriptor.py b/bleak/backends/winrt/descriptor.py deleted file mode 100644 index 1203b754b..000000000 --- a/bleak/backends/winrt/descriptor.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -import sys - -if sys.version_info >= (3, 12): - from winrt.windows.devices.bluetooth.genericattributeprofile import GattDescriptor -else: - from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattDescriptor, - ) - -from ..descriptor import BleakGATTDescriptor - - -class BleakGATTDescriptorWinRT(BleakGATTDescriptor): - """GATT Descriptor implementation for .NET backend, implemented with WinRT""" - - def __init__( - self, obj: GattDescriptor, characteristic_uuid: str, characteristic_handle: int - ): - super(BleakGATTDescriptorWinRT, self).__init__(obj) - self.obj = obj - self.__characteristic_uuid = characteristic_uuid - self.__characteristic_handle = characteristic_handle - - @property - def characteristic_handle(self) -> int: - """handle for the characteristic that this descriptor belongs to""" - return self.__characteristic_handle - - @property - def characteristic_uuid(self) -> str: - """UUID for the characteristic that this descriptor belongs to""" - return self.__characteristic_uuid - - @property - def uuid(self) -> str: - """UUID for this descriptor""" - return str(self.obj.uuid) - - @property - def handle(self) -> int: - """Integer handle for this descriptor""" - return self.obj.attribute_handle diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index 723ae1fe1..d25318f2e 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -1,32 +1,39 @@ +import sys +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + if sys.platform != "win32": + assert False, "This backend is only available on Windows" + import asyncio import logging -import sys -from typing import Dict, List, Literal, NamedTuple, Optional +from typing import Literal, NamedTuple, Optional from uuid import UUID -from .util import assert_mta - -if sys.version_info >= (3, 12): - from winrt.windows.devices.bluetooth.advertisement import ( - BluetoothLEAdvertisementReceivedEventArgs, - BluetoothLEAdvertisementType, - BluetoothLEAdvertisementWatcher, - BluetoothLEAdvertisementWatcherStatus, - BluetoothLEScanningMode, - ) +if sys.version_info < (3, 12): + from typing_extensions import override else: - from bleak_winrt.windows.devices.bluetooth.advertisement import ( - BluetoothLEAdvertisementReceivedEventArgs, - BluetoothLEAdvertisementType, - BluetoothLEAdvertisementWatcher, - BluetoothLEAdvertisementWatcherStatus, - BluetoothLEScanningMode, - ) - -from ...assigned_numbers import AdvertisementDataType -from ...exc import BleakError -from ...uuids import normalize_uuid_str -from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner + from typing import override + +from winrt.windows.devices.bluetooth.advertisement import ( + BluetoothLEAdvertisementReceivedEventArgs, + BluetoothLEAdvertisementType, + BluetoothLEAdvertisementWatcher, + BluetoothLEAdvertisementWatcherStatus, + BluetoothLEAdvertisementWatcherStoppedEventArgs, + BluetoothLEScanningMode, +) +from winrt.windows.foundation import EventRegistrationToken + +from bleak.assigned_numbers import AdvertisementDataType +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BaseBleakScanner, +) +from bleak.backends.winrt.util import assert_mta +from bleak.exc import BleakError +from bleak.uuids import normalize_uuid_str logger = logging.getLogger(__name__) @@ -42,7 +49,7 @@ def _format_event_args(e: BluetoothLEAdvertisementReceivedEventArgs) -> str: return _format_bdaddr(e.bluetooth_address) -class _RawAdvData(NamedTuple): +class RawAdvData(NamedTuple): """ Platform-specific advertisement data. @@ -80,15 +87,15 @@ class BleakScannerWinRT(BaseBleakScanner): def __init__( self, detection_callback: Optional[AdvertisementDataCallback], - service_uuids: Optional[List[str]], + service_uuids: Optional[list[str]], scanning_mode: Literal["active", "passive"], - **kwargs, + **kwargs: Any, ): super(BleakScannerWinRT, self).__init__(detection_callback, service_uuids) self.watcher: Optional[BluetoothLEAdvertisementWatcher] = None - self._advertisement_pairs: Dict[int, _RawAdvData] = {} - self._stopped_event = None + self._advertisement_pairs: dict[str, RawAdvData] = {} + self._stopped_event: Optional[asyncio.Event] = None # case insensitivity is for backwards compatibility on Windows only if scanning_mode.lower() == "passive": @@ -104,8 +111,8 @@ def __init__( self._signal_strength_filter = kwargs.get("SignalStrengthFilter", None) self._advertisement_filter = kwargs.get("AdvertisementFilter", None) - self._received_token = None - self._stopped_token = None + self._received_token: Optional[EventRegistrationToken] = None + self._stopped_token: Optional[EventRegistrationToken] = None def _received_handler( self, @@ -127,23 +134,25 @@ def _received_handler( # us (regular advertisement + scan response) so we have to do it manually. # get the previous advertising data/scan response pair or start a new one - raw_data = self._advertisement_pairs.get(bdaddr, _RawAdvData(None, None)) + raw_data = self._advertisement_pairs.get(bdaddr, RawAdvData(None, None)) # update the advertising data depending on the advertising data type if event_args.advertisement_type == BluetoothLEAdvertisementType.SCAN_RESPONSE: - raw_data = _RawAdvData(raw_data.adv, event_args) + raw_data = RawAdvData(raw_data.adv, event_args) else: - raw_data = _RawAdvData(event_args, raw_data.scan) + raw_data = RawAdvData(event_args, raw_data.scan) self._advertisement_pairs[bdaddr] = raw_data - uuids = [] + uuids: list[str] = [] mfg_data = {} service_data = {} local_name = None tx_power = None for args in filter(lambda d: d is not None, raw_data): + assert args + for u in args.advertisement.service_uuids: uuids.append(str(u)) @@ -155,8 +164,8 @@ def _received_handler( local_name = args.advertisement.local_name try: - if args.transmit_power_level_in_d_bm is not None: - tx_power = args.transmit_power_level_in_d_bm + if args.transmit_power_level_in_dbm is not None: + tx_power = args.transmit_power_level_in_dbm except AttributeError: # the transmit_power_level_in_d_bm property was introduce in # Windows build 19041 so we have a fallback for older versions @@ -198,24 +207,30 @@ def _received_handler( service_data=service_data, service_uuids=uuids, tx_power=tx_power, - rssi=event_args.raw_signal_strength_in_d_bm, + rssi=event_args.raw_signal_strength_in_dbm, platform_data=(sender, raw_data), ) device = self.create_or_update_device( - bdaddr, local_name, raw_data, advertisement_data + bdaddr, bdaddr, local_name, raw_data, advertisement_data ) self.call_detection_callbacks(device, advertisement_data) - def _stopped_handler(self, sender, e): + def _stopped_handler( + self, + sender: BluetoothLEAdvertisementWatcher, + e: BluetoothLEAdvertisementWatcherStoppedEventArgs, + ) -> None: logger.debug( "%s devices found. Watcher status: %r.", len(self.seen_devices), sender.status, ) + assert self._stopped_event self._stopped_event.set() + @override async def start(self) -> None: if self.watcher: raise BleakError("Scanner already started") @@ -230,16 +245,27 @@ async def start(self) -> None: self.watcher = BluetoothLEAdvertisementWatcher() self.watcher.scanning_mode = self._scanning_mode + # BlueZ and CoreBluetooth don't allow controlling this and always enabled it, so do the same here + self.watcher.allow_extended_advertisements = True event_loop = asyncio.get_running_loop() self._stopped_event = asyncio.Event() - self._received_token = self.watcher.add_received( - lambda s, e: event_loop.call_soon_threadsafe(self._received_handler, s, e) - ) - self._stopped_token = self.watcher.add_stopped( - lambda s, e: event_loop.call_soon_threadsafe(self._stopped_handler, s, e) - ) + def on_received( + sender: BluetoothLEAdvertisementWatcher, + args: BluetoothLEAdvertisementReceivedEventArgs, + ) -> None: + event_loop.call_soon_threadsafe(self._received_handler, sender, args) + + self._received_token = self.watcher.add_received(on_received) + + def on_stopped( + sender: BluetoothLEAdvertisementWatcher, + args: BluetoothLEAdvertisementWatcherStoppedEventArgs, + ) -> None: + event_loop.call_soon_threadsafe(self._stopped_handler, sender, args) + + self._stopped_token = self.watcher.add_stopped(on_stopped) if self._signal_strength_filter is not None: self.watcher.signal_strength_filter = self._signal_strength_filter @@ -258,7 +284,13 @@ async def start(self) -> None: if self.watcher.status != BluetoothLEAdvertisementWatcherStatus.STARTED: raise BleakError(f"Unexpected watcher status: {self.watcher.status.name}") + @override async def stop(self) -> None: + assert self.watcher + assert self._stopped_event + assert self._received_token + assert self._stopped_token + self.watcher.stop() if self.watcher.status == BluetoothLEAdvertisementWatcherStatus.STOPPING: @@ -279,22 +311,3 @@ async def stop(self) -> None: self._received_token = None self.watcher = None - - def set_scanning_filter(self, **kwargs) -> None: - """Set a scanning filter for the BleakScanner. - - Keyword Args: - SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A - BluetoothSignalStrengthFilter object used for configuration of Bluetooth - LE advertisement filtering that uses signal strength-based filtering. - AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A - BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE - advertisement filtering that uses payload section-based filtering. - - """ - if "SignalStrengthFilter" in kwargs: - # TODO: Handle SignalStrengthFilter parameters - self._signal_strength_filter = kwargs["SignalStrengthFilter"] - if "AdvertisementFilter" in kwargs: - # TODO: Handle AdvertisementFilter parameters - self._advertisement_filter = kwargs["AdvertisementFilter"] diff --git a/bleak/backends/winrt/service.py b/bleak/backends/winrt/service.py deleted file mode 100644 index dde2d4ea2..000000000 --- a/bleak/backends/winrt/service.py +++ /dev/null @@ -1,42 +0,0 @@ -import sys -from typing import List - -if sys.version_info >= (3, 12): - from winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattDeviceService, - ) -else: - from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattDeviceService, - ) - -from ..service import BleakGATTService -from ..winrt.characteristic import BleakGATTCharacteristicWinRT - - -class BleakGATTServiceWinRT(BleakGATTService): - """GATT Characteristic implementation for the .NET backend, implemented with WinRT""" - - def __init__(self, obj: GattDeviceService): - super().__init__(obj) - self.__characteristics = [] - - @property - def uuid(self) -> str: - return str(self.obj.uuid) - - @property - def handle(self) -> int: - return self.obj.attribute_handle - - @property - def characteristics(self) -> List[BleakGATTCharacteristicWinRT]: - """List of characteristics for this service""" - return self.__characteristics - - def add_characteristic(self, characteristic: BleakGATTCharacteristicWinRT): - """Add a :py:class:`~BleakGATTCharacteristicWinRT` to the service. - - Should not be used by end user, but rather by `bleak` itself. - """ - self.__characteristics.append(characteristic) diff --git a/bleak/backends/winrt/util.py b/bleak/backends/winrt/util.py index 905379448..026649cb9 100644 --- a/bleak/backends/winrt/util.py +++ b/bleak/backends/winrt/util.py @@ -1,11 +1,16 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "win32": + assert False, "This backend is only available on Windows" + import asyncio import ctypes -import sys from ctypes import wintypes from enum import IntEnum -from typing import Tuple -from ...exc import BleakError +from bleak.exc import BleakError if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout @@ -13,14 +18,14 @@ from asyncio import timeout as async_timeout -def _check_result(result, func, args): +def _check_result(result: int, func, args): if not result: raise ctypes.WinError() return args -def _check_hresult(result, func, args): +def _check_hresult(result: int, func, args): if result: raise ctypes.WinError(result) @@ -99,7 +104,7 @@ class _AptQualifierType(IntEnum): RESERVED_1 = 7 -def _get_apartment_type() -> Tuple[_AptType, _AptQualifierType]: +def _get_apartment_type() -> tuple[_AptType, _AptQualifierType]: """ Calls CoGetApartmentType to get the current apartment type and qualifier. diff --git a/bleak/exc.py b/bleak/exc.py index d03d20358..73906eff2 100644 --- a/bleak/exc.py +++ b/bleak/exc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import uuid -from typing import Optional, Union +from typing import Any, Optional, Union class BleakError(Exception): @@ -49,7 +49,7 @@ def __init__(self, identifier: str, *args: object) -> None: class BleakDBusError(BleakError): """Specialized exception type for D-Bus errors.""" - def __init__(self, dbus_error: str, error_body: list): + def __init__(self, dbus_error: str, error_body: list[Any]): """ Args: dbus_error (str): The D-Bus error, e.g. ``org.freedesktop.DBus.Error.UnknownObject``. @@ -171,7 +171,7 @@ def __str__(self) -> str: 0x0C: "Insufficient Encryption Key Size", 0x0D: "Invalid Attribute Value Length", 0x0E: "Unlikely Error", - 0x0F: "Insufficient Authentication", + 0x0F: "Insufficient Encryption", 0x10: "Unsupported Group Type", 0x11: "Insufficient Resource", 0x12: "Database Out Of Sync", diff --git a/bleak/uuids.py b/bleak/uuids.py index 1e91aacc4..4f4c2a3e6 100644 --- a/bleak/uuids.py +++ b/bleak/uuids.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -from typing import Dict from uuid import UUID -uuid16_dict: Dict[int, str] = { +uuid16_dict: dict[int, str] = { 0x0001: "SDP", 0x0003: "RFCOMM", 0x0005: "TCS-BIN", @@ -1028,7 +1027,7 @@ 0xFFFE: "Alliance for Wireless Power (A4WP)", } -uuid128_dict: Dict[str, str] = { +uuid128_dict: dict[str, str] = { "a3c87500-8ed3-4bdf-8a39-a01bebede295": "Eddystone Configuration Service", "a3c87501-8ed3-4bdf-8a39-a01bebede295": "Capabilities", "a3c87502-8ed3-4bdf-8a39-a01bebede295": "Active Slot", @@ -1193,7 +1192,7 @@ def uuidstr_to_str(uuid_: str) -> str: return s -def register_uuids(uuids_to_descriptions: Dict[str, str]) -> None: +def register_uuids(uuids_to_descriptions: dict[str, str]) -> None: """Add or modify the mapping of 128-bit UUIDs for services and characteristics to descriptions. Args: diff --git a/docs/.isort.cfg b/docs/.isort.cfg new file mode 100644 index 000000000..435db91d8 --- /dev/null +++ b/docs/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +py_version=311 diff --git a/docs/api/args.rst b/docs/api/args.rst new file mode 100644 index 000000000..da63e9e38 --- /dev/null +++ b/docs/api/args.rst @@ -0,0 +1,16 @@ +============ +Args modules +============ + +In order to use some platform-specific features, Bleak provides platform-specific +arguments to various constructors and methods. This modules all live in the +``bleak.args`` sub-package. + +.. automodule:: bleak.args.bluez + :members: + +.. automodule:: bleak.args.corebluetooth + :members: + +.. automodule:: bleak.args.winrt + :members: diff --git a/docs/api/client.rst b/docs/api/client.rst index b255f1e76..8ca94304c 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -2,6 +2,8 @@ BleakClient class ================= +.. currentmodule:: bleak + .. autoclass:: bleak.BleakClient ---------------------------- @@ -101,14 +103,11 @@ On some devices, some characteristics may require authentication in order to read or write the characteristic. In this case pairing/bonding the device is required. +.. tip:: If you need to pair the device *before* connecting, pass ``pair=True`` + to the :class:`BleakClient` constructor. Then pairing will happen during + the connection process and you do not need to call the :meth:`pair ` + method explicitly. .. automethod:: bleak.BleakClient.pair .. automethod:: bleak.BleakClient.unpair - ----------- -Deprecated ----------- - -.. automethod:: bleak.BleakClient.set_disconnected_callback -.. automethod:: bleak.BleakClient.get_services diff --git a/docs/api/index.rst b/docs/api/index.rst index e80bcc8bd..9a66afbb6 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -10,6 +10,7 @@ Contents: scanner client + args .. TODO: move everything below to separate pages @@ -45,10 +46,3 @@ Utilities .. automodule:: bleak.uuids :members: - -Deprecated ----------- - -.. module:: bleak - -.. autofunction:: bleak.discover diff --git a/docs/api/scanner.rst b/docs/api/scanner.rst index 1c232e717..c82dc5162 100644 --- a/docs/api/scanner.rst +++ b/docs/api/scanner.rst @@ -81,11 +81,3 @@ Otherwise, you can use one of the properties below after scanning has stopped. .. autoproperty:: bleak.BleakScanner.discovered_devices .. autoproperty:: bleak.BleakScanner.discovered_devices_and_advertisement_data - ----------- -Deprecated ----------- - -.. automethod:: bleak.BleakScanner.register_detection_callback -.. automethod:: bleak.BleakScanner.set_scanning_filter -.. automethod:: bleak.BleakScanner.get_discovered_devices diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 2afbb68d1..043219772 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -4,8 +4,8 @@ Backend implementations Bleak supports the following operating systems: * Windows 10, version 16299 (Fall Creators Update) and greater -* Linux distributions with BlueZ >= 5.43 (See :ref:`linux-backend` for more details) -* OS X/macOS support via Core Bluetooth API, from at least version 10.11 +* Linux distributions with BlueZ >= 5.55 (See :ref:`linux-backend` for more details) +* OS X/macOS support via Core Bluetooth API, from at least version 10.13 * Partial Android support mostly using Python-for-Android/Kivy. These pages document platform specific differences from the interface API. diff --git a/docs/backends/linux.rst b/docs/backends/linux.rst index a967bdbac..13d5d6b2a 100644 --- a/docs/backends/linux.rst +++ b/docs/backends/linux.rst @@ -9,18 +9,6 @@ over DBus. Communication uses the `dbus-fast DBus messaging. -Special handling for ``write_gatt_char`` ----------------------------------------- - -The ``type`` option to the ``Characteristic.WriteValue`` -method was added to -`Bluez in 5.51 `_ -Before that commit, ``Characteristic.WriteValue`` was only "Write with response". - -``Characteristic.AcquireWrite`` was added in -`Bluez 5.46 `_ -which can be used to "Write without response", but for older versions of Bluez (5.43, 5.44, 5.45), it is not possible to "Write without response". - Resolving services with ``get_services`` ---------------------------------------- diff --git a/docs/backends/macos.rst b/docs/backends/macos.rst index f4af7a016..a4abe5ed4 100644 --- a/docs/backends/macos.rst +++ b/docs/backends/macos.rst @@ -27,20 +27,68 @@ In the example files, this is handled in this fashion: As stated above, this will however only work the macOS machine that performed the scan and thus cached the device as ``243E23AE-4A99-406C-B317-18F1BD7B4CBE``. -There is also no pairing functionality implemented in macOS right now, since it does not seem -to be any explicit pairing methods in the COre Bluetooth. +Pairing +^^^^^^^ +There is no pairing functionality implemented in macOS right now, since it does not seem +to be any explicit pairing methods in CoreBluetooth. + +Instead, macOS will prompt the user the first time a characteristic that requires +authorization/authentication is accessed. This means that a GATT read or write +operation could block for a long time waiting for the user to responsed. So +timeouts should be set accordingly. + +Calling the :meth:`bleak.BleakClient.pair` method will raise a ``NotImplementedError`` +on macOS. But setting ``pair=True`` in :class:`bleak.BleakClient` will be silently ignored. + +.. _cb-notification-discriminator: + +Notifications +^^^^^^^^^^^^^ +CoreBluetooth does not differentiate between data from a notification and data from a read. +This can cause confusion in cases where a device may send a notification message on a characteristic +as a signal that the characteristic needs to be read again. + +Bleak can accept a ``notification_discriminator`` callback in the ``cb`` dict parameter that is +passed to the :meth:`bleak.BleakClient.start_notify` method that can differentiate between these types of data. + +.. code-block:: python + + event = asyncio.Event() + + async def notification_handler(char, data): + event.set() + + def notification_check_handler(data): + # We can identify notifications on this characteristic because they + # only contain 1 byte of data. Read responses will have more than + # 1 byte. + return len(data) == 1 + + await client.start_notify( + char, + notification_handler, + cb={"notification_discriminator": notification_check_handler}, + ) + + while True: + await event.wait() + # We received a notification - prepare to receive another + event.clear() + # Then read the characteristic to get the full value + data = await client.read_gatt_char(char) + # Do stuff with data API --- Scanner -~~~~~~~ +^^^^^^^ .. automodule:: bleak.backends.corebluetooth.scanner :members: Client -~~~~~~ +^^^^^^ .. automodule:: bleak.backends.corebluetooth.client :members: diff --git a/docs/conf.py b/docs/conf.py index 9da6b5463..fc1edc169 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,8 +16,7 @@ import os import pathlib import sys - -import tomli +import tomllib PROJECT_ROOT_DIR = pathlib.Path(__file__).parent.parent.resolve() @@ -67,7 +66,7 @@ # # The full version, including alpha/beta/rc tags. with open(PROJECT_ROOT_DIR / "pyproject.toml", "rb") as f: - release = tomli.load(f)["tool"]["poetry"]["version"] + release = tomllib.load(f)["project"]["version"] # The short X.Y version. version = ".".join(release.split(".")[:2]) @@ -83,7 +82,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ["_build"] +exclude_patterns = ["_build", ".venv"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -115,7 +114,6 @@ autodoc_mock_imports = [ "android", "async_timeout", - "bleak_winrt", "winrt", "CoreBluetooth", "dbus_fast", @@ -214,14 +212,14 @@ # -- Options for LaTeX output ------------------------------------------ -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # 'preamble': '', -} +# latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +# 'papersize': 'letterpaper', +# The font size ('10pt', '11pt' or '12pt'). +# 'pointsize': '10pt', +# Additional stuff for the LaTeX preamble. +# 'preamble': '', +# } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass diff --git a/docs/index.rst b/docs/index.rst index 5afd1ae4d..f1b6eb1ba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,7 +38,7 @@ Features * Supports Windows 10, version 16299 (Fall Creators Update) or greater * Supports Linux distributions with BlueZ >= 5.43 (See :ref:`linux-backend` for more details) -* OS X/macOS support via Core Bluetooth API, from at least OS X version 10.11 +* OS X/macOS support via Core Bluetooth API, from at least OS X version 10.13 Bleak supports reading, writing and getting notifications from GATT servers, as well as a function for discovering BLE devices. diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 4a774bae1..000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Sphinx==5.1.1 -sphinx-rtd-theme==1.0.0 -tomli==2.0.1 diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 53493325a..9998c6072 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -122,13 +122,14 @@ To fix the error, change the name of the script to something other than ``bleak. Linux Bugs ---------- -Occasional "Not connected" errors on Raspberry Pi -================================================= +Occasional "Not connected" errors or missing advertisments on Raspberry Pi +========================================================================== -If you are using the built in WiFi/Bluetooth module on a Raspberry Pi and and are seeing occasional -"Not connected" errors, also manifesting as HCI error 0x3e and BlueZ error "Software caused -connection abort", when trying to connect to a device, it may be due to wifi interference on -the chip level. +If you are using the built in WiFi/Bluetooth module on a Raspberry Pi and are +seeing occasional "Not connected" errors, also manifesting as HCI error 0x3e +and BlueZ error "Software caused connection abort", when trying to connect to +a device, it may be due to wifi interference on the chip level. This can also +cause advertisements to be missed when scanning. As a test the wifi interface can be disabled using either @@ -145,6 +146,9 @@ or See `this `_ Matter issue with similar problems on Raspberry Pi and other devices. +If you need Wi-Fi, you can possibly work around the issue by using a USB +Bluetooth adapter instead. + ---------- macOS Bugs ---------- diff --git a/examples/async_callback_with_queue.py b/examples/async_callback_with_queue.py index 262854b5d..2a8e65d08 100644 --- a/examples/async_callback_with_queue.py +++ b/examples/async_callback_with_queue.py @@ -14,8 +14,10 @@ import asyncio import logging import time +from typing import Optional from bleak import BleakClient, BleakScanner +from bleak.backends.characteristic import BleakGATTCharacteristic logger = logging.getLogger(__name__) @@ -24,27 +26,40 @@ class DeviceNotFoundError(Exception): pass -async def run_ble_client(args: argparse.Namespace, queue: asyncio.Queue): +class Args(argparse.Namespace): + name: Optional[str] + address: Optional[str] + characteristic: str + macos_use_bdaddr: bool + services: list[str] + debug: bool + + +async def run_ble_client( + args: Args, queue: asyncio.Queue[tuple[float, Optional[bytearray]]] +): logger.info("starting scan...") if args.address: device = await BleakScanner.find_device_by_address( - args.address, cb=dict(use_bdaddr=args.macos_use_bdaddr) + args.address, cb={"use_bdaddr": args.macos_use_bdaddr} ) if device is None: logger.error("could not find device with address '%s'", args.address) raise DeviceNotFoundError - else: + elif args.name: device = await BleakScanner.find_device_by_name( - args.name, cb=dict(use_bdaddr=args.macos_use_bdaddr) + args.name, cb={"use_bdaddr": args.macos_use_bdaddr} ) if device is None: logger.error("could not find device with name '%s'", args.name) raise DeviceNotFoundError + else: + raise ValueError("Either --name or --address must be provided") logger.info("connecting to device...") - async def callback_handler(_, data): + async def callback_handler(_: BleakGATTCharacteristic, data: bytearray) -> None: await queue.put((time.time(), data)) async with BleakClient(device) as client: @@ -58,7 +73,7 @@ async def callback_handler(_, data): logger.info("disconnected") -async def run_queue_consumer(queue: asyncio.Queue): +async def run_queue_consumer(queue: asyncio.Queue[tuple[float, Optional[bytearray]]]): logger.info("Starting queue consumer") while True: @@ -73,8 +88,8 @@ async def run_queue_consumer(queue: asyncio.Queue): logger.info("Received callback data via async queue at %s: %r", epoch, data) -async def main(args: argparse.Namespace): - queue = asyncio.Queue() +async def main(args: Args): + queue = asyncio.Queue[tuple[float, Optional[bytearray]]]() client_task = run_ble_client(args, queue) consumer_task = run_queue_consumer(queue) @@ -121,7 +136,7 @@ async def main(args: argparse.Namespace): help="sets the logging level to debug", ) - args = parser.parse_args() + args = parser.parse_args(namespace=Args()) log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( diff --git a/examples/detection_callback.py b/examples/detection_callback.py index d0cc8e059..c848ec2cb 100644 --- a/examples/detection_callback.py +++ b/examples/detection_callback.py @@ -19,13 +19,21 @@ logger = logging.getLogger(__name__) +class Args(argparse.Namespace): + macos_use_bdaddr: bool + services: list[str] + debug: bool + + def simple_callback(device: BLEDevice, advertisement_data: AdvertisementData): - logger.info("%s: %r", device.address, advertisement_data) + logger.info( + "addr: %s, name: %s, %r", device.address, device.name, advertisement_data + ) -async def main(args: argparse.Namespace): +async def main(args: Args): scanner = BleakScanner( - simple_callback, args.services, cb=dict(use_bdaddr=args.macos_use_bdaddr) + simple_callback, args.services, cb={"use_bdaddr": args.macos_use_bdaddr} ) while True: @@ -57,7 +65,7 @@ async def main(args: argparse.Namespace): help="sets the logging level to debug", ) - args = parser.parse_args() + args = parser.parse_args(namespace=Args()) log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( diff --git a/examples/disconnect_callback.py b/examples/disconnect_callback.py index e1dcb4bcb..b10e31195 100644 --- a/examples/disconnect_callback.py +++ b/examples/disconnect_callback.py @@ -11,33 +11,43 @@ import argparse import asyncio import logging +from typing import Optional from bleak import BleakClient, BleakScanner logger = logging.getLogger(__name__) -async def main(args: argparse.Namespace): +class Args(argparse.Namespace): + name: Optional[str] + address: Optional[str] + macos_use_bdaddr: bool + debug: bool + + +async def main(args: Args): logger.info("scanning...") if args.address: device = await BleakScanner.find_device_by_address( - args.address, cb=dict(use_bdaddr=args.macos_use_bdaddr) + args.address, cb={"use_bdaddr": args.macos_use_bdaddr} ) if device is None: logger.error("could not find device with address '%s'", args.address) return - else: + elif args.name: device = await BleakScanner.find_device_by_name( - args.name, cb=dict(use_bdaddr=args.macos_use_bdaddr) + args.name, cb={"use_bdaddr": args.macos_use_bdaddr} ) if device is None: logger.error("could not find device with name '%s'", args.name) return + else: + raise ValueError("Either --name or --address must be provided") disconnected_event = asyncio.Event() - def disconnected_callback(client): + def disconnected_callback(client: BleakClient): logger.info("Disconnected callback called!") disconnected_event.set() @@ -78,7 +88,7 @@ def disconnected_callback(client): help="sets the log level to debug", ) - args = parser.parse_args() + args = parser.parse_args(namespace=Args()) log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( diff --git a/examples/discover.py b/examples/discover.py index 6e8e99f55..57e72bad3 100644 --- a/examples/discover.py +++ b/examples/discover.py @@ -14,13 +14,18 @@ from bleak import BleakScanner -async def main(args: argparse.Namespace): +class Args(argparse.Namespace): + macos_use_bdaddr: bool + services: list[str] + + +async def main(args: Args): print("scanning for 5 seconds, please wait...") devices = await BleakScanner.discover( return_adv=True, service_uuids=args.services, - cb=dict(use_bdaddr=args.macos_use_bdaddr), + cb={"use_bdaddr": args.macos_use_bdaddr}, ) for d, a in devices.values(): @@ -46,6 +51,6 @@ async def main(args: argparse.Namespace): help="when true use Bluetooth address instead of UUID on macOS", ) - args = parser.parse_args() + args = parser.parse_args(namespace=Args()) asyncio.run(main(args)) diff --git a/examples/enable_notifications.py b/examples/enable_notifications.py index 93bc13d8d..43de5ed74 100644 --- a/examples/enable_notifications.py +++ b/examples/enable_notifications.py @@ -12,6 +12,7 @@ import argparse import asyncio import logging +from typing import Optional from bleak import BleakClient, BleakScanner from bleak.backends.characteristic import BleakGATTCharacteristic @@ -19,28 +20,38 @@ logger = logging.getLogger(__name__) +class Args(argparse.Namespace): + name: Optional[str] + address: Optional[str] + macos_use_bdaddr: bool + characteristic: str + debug: bool + + def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray): """Simple notification handler which prints the data received.""" logger.info("%s: %r", characteristic.description, data) -async def main(args: argparse.Namespace): +async def main(args: Args): logger.info("starting scan...") if args.address: device = await BleakScanner.find_device_by_address( - args.address, cb=dict(use_bdaddr=args.macos_use_bdaddr) + args.address, cb={"use_bdaddr": args.macos_use_bdaddr} ) if device is None: logger.error("could not find device with address '%s'", args.address) return - else: + elif args.name: device = await BleakScanner.find_device_by_name( - args.name, cb=dict(use_bdaddr=args.macos_use_bdaddr) + args.name, cb={"use_bdaddr": args.macos_use_bdaddr} ) if device is None: logger.error("could not find device with name '%s'", args.name) return + else: + raise ValueError("Either --name or --address must be provided") logger.info("connecting to device...") @@ -87,7 +98,7 @@ async def main(args: argparse.Namespace): help="sets the log level to debug", ) - args = parser.parse_args() + args = parser.parse_args(namespace=Args()) log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( diff --git a/examples/mtu_size.py b/examples/mtu_size.py index 354163300..0b4d13766 100644 --- a/examples/mtu_size.py +++ b/examples/mtu_size.py @@ -12,7 +12,7 @@ async def main(): - queue = asyncio.Queue() + queue = asyncio.Queue[BLEDevice]() def callback(device: BLEDevice, adv: AdvertisementData) -> None: # can use advertising data to filter here @@ -26,8 +26,8 @@ def callback(device: BLEDevice, adv: AdvertisementData) -> None: # BlueZ doesn't have a proper way to get the MTU, so we have this hack. # If this doesn't work for you, you can set the client._mtu_size attribute # to override the value instead. - if client._backend.__class__.__name__ == "BleakClientBlueZDBus": - await client._backend._acquire_mtu() + if client._backend.__class__.__name__ == "BleakClientBlueZDBus": # type: ignore + await client._backend._acquire_mtu() # type: ignore print("MTU:", client.mtu_size) diff --git a/examples/philips_hue.py b/examples/philips_hue.py index 9519240e8..331efb0d1 100644 --- a/examples/philips_hue.py +++ b/examples/philips_hue.py @@ -35,7 +35,7 @@ COLOR_CHARACTERISTIC = "932c32bd-0005-47a2-835a-a8d455b859dd" -def convert_rgb(rgb): +def convert_rgb(rgb: tuple[int, int, int]) -> bytearray: scale = 0xFF adjusted = [max(1, chan) for chan in rgb] total = sum(adjusted) @@ -45,12 +45,12 @@ def convert_rgb(rgb): return bytearray([0x1, adjusted[0], adjusted[2], adjusted[1]]) -async def main(address): +async def main(address: str): async with BleakClient(address) as client: print(f"Connected: {client.is_connected}") - paired = await client.pair(protection_level=2) - print(f"Paired: {paired}") + await client.pair() + print("Paired:") print("Turning Light off...") await client.write_gatt_char(LIGHT_CHARACTERISTIC, b"\x00", response=False) @@ -60,17 +60,17 @@ async def main(address): await asyncio.sleep(1.0) print("Setting color to RED...") - color = convert_rgb([255, 0, 0]) + color = convert_rgb((255, 0, 0)) await client.write_gatt_char(COLOR_CHARACTERISTIC, color, response=False) await asyncio.sleep(1.0) print("Setting color to GREEN...") - color = convert_rgb([0, 255, 0]) + color = convert_rgb((0, 255, 0)) await client.write_gatt_char(COLOR_CHARACTERISTIC, color, response=False) await asyncio.sleep(1.0) print("Setting color to BLUE...") - color = convert_rgb([0, 0, 255]) + color = convert_rgb((0, 0, 255)) await client.write_gatt_char(COLOR_CHARACTERISTIC, color, response=False) await asyncio.sleep(1.0) diff --git a/examples/sensortag.py b/examples/sensortag.py index bfae5d627..b15bcda4e 100644 --- a/examples/sensortag.py +++ b/examples/sensortag.py @@ -13,6 +13,7 @@ import sys from bleak import BleakClient +from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.uuids import normalize_uuid_16, uuid16_dict ADDRESS = ( @@ -81,8 +82,8 @@ IO_CONFIG_CHAR_UUID = "f000aa66-0451-4000-b000-000000000000" -async def main(address): - async with BleakClient(address, winrt=dict(use_cached_services=True)) as client: +async def main(address: str): + async with BleakClient(address, winrt={"use_cached_services": True}) as client: print(f"Connected: {client.is_connected}") system_id = await client.read_gatt_char(SYSTEM_ID_UUID) @@ -116,7 +117,9 @@ async def main(address): battery_level = await client.read_gatt_char(BATTERY_LEVEL_UUID) print("Battery Level: {0}%".format(int(battery_level[0]))) - async def notification_handler(characteristic, data): + async def notification_handler( + characteristic: BleakGATTCharacteristic, data: bytearray + ): print(f"{characteristic.description}: {data}") # Turn on the red light on the Sensor Tag by writing to I/O Data and I/O Config. diff --git a/examples/service_explorer.py b/examples/service_explorer.py index 5649bb545..a2ef39351 100644 --- a/examples/service_explorer.py +++ b/examples/service_explorer.py @@ -12,35 +12,50 @@ import argparse import asyncio import logging +from typing import Optional from bleak import BleakClient, BleakScanner logger = logging.getLogger(__name__) -async def main(args: argparse.Namespace): +class Args(argparse.Namespace): + name: Optional[str] + address: Optional[str] + macos_use_bdaddr: bool + services: list[str] + pair: bool + debug: bool + + +async def main(args: Args): logger.info("starting scan...") if args.address: device = await BleakScanner.find_device_by_address( - args.address, cb=dict(use_bdaddr=args.macos_use_bdaddr) + args.address, cb={"use_bdaddr": args.macos_use_bdaddr} ) if device is None: logger.error("could not find device with address '%s'", args.address) return - else: + elif args.name: device = await BleakScanner.find_device_by_name( - args.name, cb=dict(use_bdaddr=args.macos_use_bdaddr) + args.name, cb={"use_bdaddr": args.macos_use_bdaddr} ) if device is None: logger.error("could not find device with name '%s'", args.name) return + else: + raise ValueError("Either --name or --address must be provided") logger.info("connecting to device...") async with BleakClient( device, + pair=args.pair, services=args.services, + # Give the user plenty of time to enter a PIN code if paring is required. + timeout=90 if args.pair else 10, ) as client: logger.info("connected") @@ -50,7 +65,7 @@ async def main(args: argparse.Namespace): for char in service.characteristics: if "read" in char.properties: try: - value = await client.read_gatt_char(char.uuid) + value = await client.read_gatt_char(char) extra = f", Value: {value}" except Exception as e: extra = f", Error: {e}" @@ -69,7 +84,7 @@ async def main(args: argparse.Namespace): for descriptor in char.descriptors: try: - value = await client.read_gatt_descriptor(descriptor.handle) + value = await client.read_gatt_descriptor(descriptor) logger.info(" [Descriptor] %s, Value: %r", descriptor, value) except Exception as e: logger.error(" [Descriptor] %s, Error: %s", descriptor, e) @@ -108,6 +123,12 @@ async def main(args: argparse.Namespace): help="if provided, only enumerate matching service(s)", ) + parser.add_argument( + "--pair", + action="store_true", + help="pair with the device before connecting if not already paired", + ) + parser.add_argument( "-d", "--debug", @@ -115,7 +136,7 @@ async def main(args: argparse.Namespace): help="sets the log level to debug", ) - args = parser.parse_args() + args = parser.parse_args(namespace=Args()) log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( diff --git a/examples/two_devices.py b/examples/two_devices.py index 86082912e..ef86d3724 100644 --- a/examples/two_devices.py +++ b/examples/two_devices.py @@ -5,6 +5,17 @@ from typing import Iterable from bleak import BleakClient, BleakScanner +from bleak.backends.characteristic import BleakGATTCharacteristic + + +class Args(argparse.Namespace): + device1: str + uuid1: str + device2: str + uuid2: str + by_address: bool + macos_use_bdaddr: bool + debug: bool async def connect_to_device( @@ -44,7 +55,7 @@ async def connect_to_device( if by_address: device = await BleakScanner.find_device_by_address( - name_or_address, macos=dict(use_bdaddr=macos_use_bdaddr) + name_or_address, cb={"use_bdaddr": macos_use_bdaddr} ) else: device = await BleakScanner.find_device_by_name(name_or_address) @@ -55,11 +66,9 @@ async def connect_to_device( logging.error("%s not found", name_or_address) return - client = BleakClient(device) - logging.info("connecting to %s", name_or_address) - await stack.enter_async_context(client) + client = await stack.enter_async_context(BleakClient(device)) logging.info("connected to %s", name_or_address) @@ -71,7 +80,7 @@ async def connect_to_device( # Bluetooth adapter is now free to scan and connect another device # without disconnecting this one. - def callback(_, data): + def callback(_: BleakGATTCharacteristic, data: bytearray) -> None: logging.info("%s received %r", name_or_address, data) await client.start_notify(notify_uuid, callback) @@ -145,7 +154,7 @@ async def main( help="sets the log level to debug", ) - args = parser.parse_args() + args = parser.parse_args(namespace=Args()) log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( diff --git a/examples/uart_service.py b/examples/uart_service.py index 3db69dc94..1a363c71a 100644 --- a/examples/uart_service.py +++ b/examples/uart_service.py @@ -68,7 +68,9 @@ def handle_rx(_: BleakGATTCharacteristic, data: bytearray): loop = asyncio.get_running_loop() nus = client.services.get_service(UART_SERVICE_UUID) + assert nus is not None, "UART service not found" rx_char = nus.get_characteristic(UART_RX_CHAR_UUID) + assert rx_char is not None, "UART RX characteristic not found" while True: # This waits until you type a line and press ENTER. diff --git a/poetry.lock b/poetry.lock index dc89670d5..be6735fc6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,40 +1,46 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "alabaster" -version = "0.7.12" -description = "A configurable sidebar-enabled Sphinx theme" +version = "1.0.0" +description = "A light, configurable Sphinx theme" optional = false -python-versions = "*" +python-versions = ">=3.10" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] [[package]] name = "async-timeout" -version = "4.0.2" +version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.11\"" files = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] [[package]] -name = "Babel" -version = "2.10.3" +name = "babel" +version = "2.17.0" description = "Internationalization utilities" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, - {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] -[package.dependencies] -pytz = ">=2015.7" +[package.extras] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "black" @@ -42,6 +48,7 @@ version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["lint"] files = [ {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, @@ -78,36 +85,18 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "bleak-winrt" -version = "1.2.0" -description = "Python WinRT bindings for Bleak" -optional = false -python-versions = "*" -files = [ - {file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"}, - {file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"}, - {file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"}, - {file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"}, - {file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"}, - {file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"}, - {file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"}, - {file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"}, - {file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"}, - {file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"}, - {file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"}, -] - [[package]] name = "certifi" version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, @@ -119,6 +108,8 @@ version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.6.0" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, @@ -133,6 +124,7 @@ version = "8.1.3" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["lint"] files = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, @@ -143,14 +135,16 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["docs", "lint", "test"] files = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {docs = "python_version >= \"3.11\" and sys_platform == \"win32\"", lint = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} [[package]] name = "coverage" @@ -158,6 +152,7 @@ version = "6.4.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, @@ -215,60 +210,75 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "dbus-fast" -version = "2.0.0" +version = "2.44.1" description = "A faster version of dbus-next" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system == \"Linux\"" files = [ - {file = "dbus_fast-2.0.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:558748ce71696af21414b44119c4cf9d8aca1f3d7ebb493a9f4723e95dd71da1"}, - {file = "dbus_fast-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5404cbd9c50075f18aa1e412c354fd4c8ab8d4b824599ded1302231acc30990e"}, - {file = "dbus_fast-2.0.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:804004c552b271d9dc5709934ebeeeefce763fe3081f793ea163518fd1848092"}, - {file = "dbus_fast-2.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6030b22985caa030a5f873dfa8aee556506cdcb47ffdae85cf250ce2b360a11f"}, - {file = "dbus_fast-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfeddf01c8e57be89cec50d0b978a8307e1ad8402bce162b58467931e9ad6596"}, - {file = "dbus_fast-2.0.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:ec7ddb797f1c92a3089de444bb422aa749f845affae44527310d322b0c48ea65"}, - {file = "dbus_fast-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea7a2730bf7490cf50950ddf0872a5cc61c6e557bf92320f15178e7c9e9aff9"}, - {file = "dbus_fast-2.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7a72d03edae8269476aa6af11744871281f6e1b0b6d8f065ef98047c03ffa464"}, - {file = "dbus_fast-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:34c418b322f47be0313535505915df0fd5cff9ffd899d0be87edf2830cc1d339"}, - {file = "dbus_fast-2.0.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9a7005db366adf01e871a8071437218fe27fe067554c44ca3d4b389769dbbc76"}, - {file = "dbus_fast-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef8a64d5e853b0accc379dc4c6bf1b395227834adc705fa78d3417f2e09cdac2"}, - {file = "dbus_fast-2.0.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1aa133f0a70ad83ad8dfcde6ac8684e3492404c8ccc0080695855e4c2973f355"}, - {file = "dbus_fast-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:45057e9f6984ad3298c6ce4526022ce432ca44a78ff05547bac20028e9290129"}, - {file = "dbus_fast-2.0.0-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:3dc584fe6c87aa4db3b4d938795f4d70bdadaaffa8ca4967c4a68f1151f36243"}, - {file = "dbus_fast-2.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cb7027302584377c97bf6441d4ca3d35300cc3d0b49b407165eee20796d2f04"}, - {file = "dbus_fast-2.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:855f8c76cb227fc1745bf43f40899898be19ea2dc8b120e86c55f26eac07e541"}, - {file = "dbus_fast-2.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a73cf139c7c5014620186fec64d749ea566656ad4a78e4854bceff999acdb916"}, - {file = "dbus_fast-2.0.0-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:2ebe6975fc24455b672e59c4a26476f3e5e9d25350ffc7941127cf3ce23df79e"}, - {file = "dbus_fast-2.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77fe92d8588f18fcfeb5773fb8f541d1ea0064347e95a12cfe7f2d0913f4d3e9"}, - {file = "dbus_fast-2.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cc21cd1adf3aa3be205f2ee027d0d3d5b73a809de59c48027d87434309dfddde"}, - {file = "dbus_fast-2.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a4db0946a8b7129a573b9fc01b70107f51db2fbeb8aba8409d75edf59e525b6e"}, - {file = "dbus_fast-2.0.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:83af9ed1479e733ff1d5bcedde1d689d02ff746ef587bc3e9aa243b7a464a0c3"}, - {file = "dbus_fast-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f5c93343b580be44caa83271331e45aee0ce329461f132a1be581679cddf6c5"}, - {file = "dbus_fast-2.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7779a91aa5e1b0811658d672ae36d0a66aa1d2e693a113713ef1f4e769913e67"}, - {file = "dbus_fast-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3a0d5c6e9b874bf2ab509d9c5116f1d0067fbf48e72689c9eecca2de0939ec67"}, - {file = "dbus_fast-2.0.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4ed0dce64aa6ecf0076524942184cc7ca50166c67930eb0467989ca5d5c4cfa1"}, - {file = "dbus_fast-2.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e515ed03adc94a76caf12ce93bf53539ae8b9e2960d0fc329e258249a0767a15"}, - {file = "dbus_fast-2.0.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4efb676f5c577853ea1b45ac88fa6c799504b651138926e8714e7e2b40b848c7"}, - {file = "dbus_fast-2.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29d7b559d780fc19b5250b8358e43c557227e36bacb27c3cae40c44d24589b5e"}, - {file = "dbus_fast-2.0.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:683fbd87f76099fa767a01c7e6a98f0214a8746f4a83aee6b57adec23ae2948f"}, - {file = "dbus_fast-2.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8df17f12c8d32e0cd0f32ff9691b9dcc1fb6024750de62f23c1c571256cbe87d"}, - {file = "dbus_fast-2.0.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:1bb1d8c582f53c0051b9f8ffaceb1b01e0e3fb2acd5a80a70c3a4b21488fe4da"}, - {file = "dbus_fast-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3552250763864f285899c1ef0153a58bf0e1fe5644e7dc43772132890300cbd"}, - {file = "dbus_fast-2.0.0.tar.gz", hash = "sha256:bb8bfdc01d50be88598f58bab2e1dea72b0730e026b3c39260dbf8f6c64b88bd"}, + {file = "dbus_fast-2.44.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c78a004ba43aeaf203a19169d2b4be238375905645999da30cb0da730df80cf2"}, + {file = "dbus_fast-2.44.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65a634286651398f3f1326e8200fc54289d52c2c00249d29cacfc691660a5da1"}, + {file = "dbus_fast-2.44.1-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0c4a128f8b29941307fc5722f37a1bb87ddcf733188d917ab374d9da0c6e1ce7"}, + {file = "dbus_fast-2.44.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adaf459fbce22a63d3578f3ec782c6978edf975eb06d71fb5b7a690496cf6bbe"}, + {file = "dbus_fast-2.44.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de871cf722c436bdcceb96b2a3af7084e1fa468f7916ae278ec8ec49a6fa7eef"}, + {file = "dbus_fast-2.44.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b40863de172031bcc02f54c6f05cccb0b882dc2e1b09e11314a8ccf38c558760"}, + {file = "dbus_fast-2.44.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b7ae16555df6b56d3befcc51e036779ef47c0e954fdb9fb0821ac25212aefe9"}, + {file = "dbus_fast-2.44.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a220a28e88062a2548f0c6da9eb15fb7e3af70eae56729fc3795ce3e3fba057d"}, + {file = "dbus_fast-2.44.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ec5db912bd4cfeadf7134163d6dde684271cd44cf26e3b4720107f3de406623"}, + {file = "dbus_fast-2.44.1-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:6ad99f626837753b39a39e09facd2091ee4851ee1eb6ebec5fa9a9a231734254"}, + {file = "dbus_fast-2.44.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7aa157f689a114bfb5367c55884d35e25d57cf25202a6590ce05010f929e7df"}, + {file = "dbus_fast-2.44.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f961d8bcad80359f24c0156b3094f58a87d583d56139ee50922fe5894b6797cf"}, + {file = "dbus_fast-2.44.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1f38fb5c31846c3ada8fc2b693d8d19953d376a9ea21079e3686e93faa1f8a0f"}, + {file = "dbus_fast-2.44.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35e3cde53cc9180ce95c6c84a1e8d1ded429031e4a0a182606e8d22cf57d3294"}, + {file = "dbus_fast-2.44.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f30fb09f1ea13658fb4316511e27d6b94f8363b16f2d093efe73e6e289b740"}, + {file = "dbus_fast-2.44.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dd0f8d41f6ab9d4a782c116470bc319d690f9b50c97b6debc6d1fef08e4615a"}, + {file = "dbus_fast-2.44.1-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9d6e386658343db380b9e4e81b3bf4e3c17135dbb5889173b1f2582b675b9a8c"}, + {file = "dbus_fast-2.44.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bd27563c11219b6fde7a5458141d860d8445c2defb036bab360d1f9bf1dfae0"}, + {file = "dbus_fast-2.44.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0272784aceac821dd63c8187a8860179061a850269617ff5c5bd25ca37bf9307"}, + {file = "dbus_fast-2.44.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eed613a909a45f0e0a415c88b373024f007a9be56b1316812ed616d69a3b9161"}, + {file = "dbus_fast-2.44.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0d4288f2cba4f8309dcfd9f4392e0f4f2b5be6c796dfdb0c5e03228b1ab649b1"}, + {file = "dbus_fast-2.44.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50a9a4c6921f4b7446717fb4869750f54b561ce486b25b36550cb2a910c988d9"}, + {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89dc5db158bf9838979f732acc39e0e1ecd7e3295a09fa8adb93b09c097615a4"}, + {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:f11878c0c089d278861e48c02db8002496c2233b0f605b5630ef61f0b7fb0ea3"}, + {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd81f483b3ffb71e88478cfabccc1fab8d7154fccb1c661bfafcff9b0cfd996"}, + {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:ad499de96a991287232749c98a59f2436ed260f6fd9ad4cb3b04a4b1bbbef148"}, + {file = "dbus_fast-2.44.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36c44286b11e83977cd29f9551b66b446bb6890dff04585852d975aa3a038ca2"}, + {file = "dbus_fast-2.44.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:89f2f6eccbb0e464b90e5a8741deb9d6a91873eeb41a8c7b963962b39eb1e0cd"}, + {file = "dbus_fast-2.44.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb74a227b071e1a7c517bf3a3e4a5a0a2660620084162e74f15010075534c9d5"}, + {file = "dbus_fast-2.44.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e3719399e687359b0ef66af1b720661dd4f12059db1c4f506e678569a2256b4"}, + {file = "dbus_fast-2.44.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:806450623ef3f8df846524da7e448edc8174261a01cfd5dfda92e3df89c0de10"}, + {file = "dbus_fast-2.44.1-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:55ad499b7ef08cb76fce9c9fdcdd6589d2ebfc7e53b3d261d8f40c6d97a8d901"}, + {file = "dbus_fast-2.44.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55d717865219ec2ae9977b6d067c05261cdc3ef6205c687c8bb92b3437886e58"}, + {file = "dbus_fast-2.44.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39d4cc61e491e11912f76d70cc1c47387ab4f2e5b71f34bfa13eb11aa6026268"}, + {file = "dbus_fast-2.44.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9b3b10151f1140f7b6dd47a89fc37edd05d6213be0a1748eadba82fc144c05c2"}, + {file = "dbus_fast-2.44.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:33772c223f5cef1bacc298e83dc04b27b3a47065b245fde766fcc126e761dca7"}, + {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e3f42f982af45bcfa0ff23e808f3aa54a45fe4bf43aadd3beb5ace816fba76"}, + {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f29a81d86c9ce3020a5df8c1e5557edaa00e1e00c9804ec874d46c99d967a686"}, + {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:5dec134715457601c0fa8df3040a56d319de1a152464ae4d4bfc53bbb5c02e04"}, + {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893509b516f2f24b4e3f09a6b1f3a30f856cf237cd773cdc505ea7ab4fa3c863"}, + {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:db81275d708774f6a17c89f2e063398c0deb358c4d22b663a3dd99861f6683a4"}, + {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:161a3e6fc8783c30c9feb072e09604d96ec0c465b06bd35b6acc1a0316bd2a27"}, + {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:67febe6454e714d85a532bd84969001ed948bbaf1699a7e1e4c6abb5508c9522"}, + {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890f0fc046d5db66524ddedeca8c14b65739fbbf32d6488175c07428362bf250"}, + {file = "dbus_fast-2.44.1.tar.gz", hash = "sha256:b027e96c39ed5622bb54d811dcdbbe9d9d6edec3454808a85a1ceb1867d9e25c"}, ] [[package]] name = "docutils" -version = "0.17.1" +version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.9" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] @@ -277,6 +287,8 @@ version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["test"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, @@ -287,19 +299,20 @@ test = ["pytest (>=6)"] [[package]] name = "flake8" -version = "5.0.4" +version = "7.1.1" description = "the modular source code checker: pep8 pyflakes and co" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8.1" +groups = ["lint"] files = [ - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, + {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, + {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.9.0,<2.10.0" -pyflakes = ">=2.5.0,<2.6.0" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" [[package]] name = "idna" @@ -307,6 +320,8 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, @@ -318,36 +333,20 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "4.12.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, - {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] - [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" optional = false python-versions = "*" +groups = ["test"] files = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -359,6 +358,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["lint"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -369,13 +369,15 @@ colors = ["colorama (>=0.4.6)"] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -390,6 +392,8 @@ version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, @@ -439,6 +443,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["lint"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -450,6 +455,7 @@ version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." optional = false python-versions = "*" +groups = ["lint"] files = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, @@ -461,10 +467,12 @@ version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" +groups = ["docs", "lint", "test"] files = [ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] +markers = {docs = "python_version >= \"3.11\""} [[package]] name = "pathspec" @@ -472,6 +480,7 @@ version = "0.10.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.7" +groups = ["lint"] files = [ {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, @@ -483,6 +492,7 @@ version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" +groups = ["lint"] files = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, @@ -498,6 +508,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -509,39 +520,43 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pycodestyle" -version = "2.9.1" +version = "2.12.1" description = "Python style guide checker" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["lint"] files = [ - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] [[package]] name = "pyflakes" -version = "2.5.0" +version = "3.2.0" description = "passive checker of Python programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["lint"] files = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] [[package]] name = "pygments" -version = "2.15.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "Pygments-2.15.0-py3-none-any.whl", hash = "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094"}, - {file = "Pygments-2.15.0.tar.gz", hash = "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] -plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyobjc-core" @@ -549,6 +564,8 @@ version = "10.3.1" description = "Python<->ObjC Interoperability Module" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_core-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ea46d2cda17921e417085ac6286d43ae448113158afcf39e0abe484c58fb3d78"}, {file = "pyobjc_core-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:899d3c84d2933d292c808f385dc881a140cf08632907845043a333a9d7c899f9"}, @@ -566,6 +583,8 @@ version = "10.3.1" description = "Wrappers for the Cocoa frameworks on macOS" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_Cocoa-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4cb4f8491ab4d9b59f5187e42383f819f7a46306a4fa25b84f126776305291d1"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5f31021f4f8fdf873b57a97ee1f3c1620dbe285e0b4eaed73dd0005eb72fd773"}, @@ -586,6 +605,8 @@ version = "10.3.1" description = "Wrappers for the framework CoreBluetooth on macOS" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_13_universal2.whl", hash = "sha256:c89ee6fba0ed359c46b4908a7d01f88f133be025bd534cbbf4fb9c183e62fc97"}, {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2f261a386aa6906f9d4601d35ff71a13315dbca1a0698bf1f1ecfe3971de4648"}, @@ -604,6 +625,8 @@ version = "10.3.1" description = "Wrappers for libdispatch on macOS" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_libdispatch-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5543aea8acd53fb02bcf962b003a2a9c2bdacf28dc290c31a3d2de7543ef8392"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3e0db3138aae333f0b87b42586bc016430a76638af169aab9cef6afee4e5f887"}, @@ -625,6 +648,7 @@ version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, @@ -647,6 +671,7 @@ version = "0.23.7" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, @@ -665,6 +690,7 @@ version = "3.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.6" +groups = ["test"] files = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, @@ -677,31 +703,22 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] -[[package]] -name = "pytz" -version = "2022.2.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, - {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, -] - [[package]] name = "requests" -version = "2.32.0" +version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, - {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" @@ -709,121 +726,172 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "roman-numerals-py" +version = "3.1.0" +description = "Manipulate well-formed Roman numerals" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c"}, + {file = "roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d"}, +] + +[package.extras] +lint = ["mypy (==1.15.0)", "pyright (==1.1.394)", "ruff (==0.9.7)"] +test = ["pytest (>=8)"] + [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] [[package]] -name = "Sphinx" -version = "5.1.1" +name = "sphinx" +version = "8.2.3" description = "Python documentation generator" optional = false -python-versions = ">=3.6" +python-versions = ">=3.11" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "Sphinx-5.1.1-py3-none-any.whl", hash = "sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693"}, - {file = "Sphinx-5.1.1.tar.gz", hash = "sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89"}, + {file = "sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3"}, + {file = "sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=1.3" -colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.20" -imagesize = "*" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} -Jinja2 = ">=2.3" -packaging = "*" -Pygments = ">=2.0" -requests = ">=2.5.0" -snowballstemmer = ">=1.1" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +roman-numerals-py = ">=1.0.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "isort", "mypy (>=0.971)", "sphinx-lint", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest (>=4.6)", "typed-ast"] +lint = ["betterproto (==2.0.0b6)", "mypy (==1.15.0)", "pypi-attestations (==0.0.21)", "pyright (==1.1.395)", "pytest (>=8.0)", "ruff (==0.9.9)", "sphinx-lint (>=0.9)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.19.0.20250219)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241128)", "types-requests (==2.32.0.20241016)", "types-urllib3 (==1.26.25.14)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "pytest-xdist[psutil] (>=3.4)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] name = "sphinx-rtd-theme" -version = "1.0.0" +version = "3.0.2" description = "Read the Docs theme for Sphinx" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.8" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, - {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, + {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, + {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, ] [package.dependencies] -docutils = "<0.18" -sphinx = ">=1.6" +docutils = ">0.18,<0.22" +sphinx = ">=6,<9" +sphinxcontrib-jquery = ">=4,<5" [package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] +dev = ["bump2version", "transifex-client", "twine", "wheel"] [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.2" -description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.0" +version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +optional = false +python-versions = ">=2.7" +groups = ["docs"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" + [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -834,330 +902,377 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["lint", "test"] files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +markers = {lint = "python_version < \"3.11\"", test = "python_full_version <= \"3.11.0a6\""} [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main", "lint"] files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] +markers = {main = "python_version < \"3.12\" or platform_system == \"Windows\"", lint = "python_version < \"3.12\""} [[package]] name = "urllib3" -version = "1.26.19" +version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.9" +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, - {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] -brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "winrt-runtime" -version = "2.2.0" +version = "3.1.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false -python-versions = "<3.14,>=3.9" +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ - {file = "winrt_runtime-2.2.0-cp310-cp310-win32.whl", hash = "sha256:ab034330d6b64ce93683bdc14d4f3f83dfafbf1f72b45893505f7d684e5e7fe1"}, - {file = "winrt_runtime-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad9927a1838dea47ceb2d773c0269242bcee7cb5379ed801547788ab435da502"}, - {file = "winrt_runtime-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:87745ae54d054957a99c70875c1ac3c89cca258ed06836ae308fbbb7dda4ef61"}, - {file = "winrt_runtime-2.2.0-cp311-cp311-win32.whl", hash = "sha256:7ee2397934c1c4a090f9d889292def90b8f673dc1d320f1f07931ad1cb6e49bf"}, - {file = "winrt_runtime-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f110b0f451b514cf09c4fa0e73bab54d4b598c3092df9dd87940403998e81f30"}, - {file = "winrt_runtime-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:27606e7a393a26e484f03db699c4d7c206d180a3736a6cd68fba3b3896e364a4"}, - {file = "winrt_runtime-2.2.0-cp312-cp312-win32.whl", hash = "sha256:5a769bfb4e264b7fd306027da90c6e4e615667e9afdd8e5d712bc45bdabaf0d2"}, - {file = "winrt_runtime-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ef30ea7446a1e37660265b76e586fcffc0e83a859b7729141cdf68cbedf808a8"}, - {file = "winrt_runtime-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:d8f6338fb8433b4df900c8f173959a5ae9ac63b0b20faddb338e76a6e9391bc9"}, - {file = "winrt_runtime-2.2.0-cp313-cp313-win32.whl", hash = "sha256:6d8c1122158edc96cac956a5ab62bc06a56e088bdf83d0993a455216b3fd1cac"}, - {file = "winrt_runtime-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:76b2dc846e6802375113c9ce9e7fcc4292926bd788445f34d404bae72d2b4f4b"}, - {file = "winrt_runtime-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:faacc05577573702cb135e7da4d619f4990c768063dc869362f13d856a0738e3"}, - {file = "winrt_runtime-2.2.0-cp39-cp39-win32.whl", hash = "sha256:f00334e3304a43e1742514bed2dc736a9242e831676f605fdfb5d62932714b18"}, - {file = "winrt_runtime-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ef1b2dc31576d686cce088a349b539fc0f47bdf2f66fb8ea63a6964dc069d00d"}, - {file = "winrt_runtime-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:1c9e8a609cf00acc426eae2ed4ad866991a0f33f196ec9dc69af95ae43b4373b"}, - {file = "winrt_runtime-2.2.0.tar.gz", hash = "sha256:37a673b295ebd5f6dc5a3b42fd52c8e4589ca3e605deb54c26d0877d2575ec85"}, + {file = "winrt_runtime-3.1.0-cp310-cp310-win32.whl", hash = "sha256:181ea97c5da9752dd6d3dac2d47739a4b537d433c59be470ae25c094c8168a6b"}, + {file = "winrt_runtime-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9111f6ff20123ad70122b56693cefb66cc5d4f162cb6a044a34819383e9be59b"}, + {file = "winrt_runtime-3.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:cb5d0b9b154299ee49426d9a1cdd8551b80e48848dd898c7a8ec053917a3b625"}, + {file = "winrt_runtime-3.1.0-cp311-cp311-win32.whl", hash = "sha256:32dfe4190ef1d4c110487588acb4a7fcaf4cc2bbbbc4bbbbe31e7169e67cce38"}, + {file = "winrt_runtime-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:216dcf6d7e07448cca6a87ee7a53aba29a9a187043d0a61db11d0a692942344d"}, + {file = "winrt_runtime-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:d5abb15de1294147ae1d3264e611ec23104e6635e835429d2c24e4d4d6f52788"}, + {file = "winrt_runtime-3.1.0-cp312-cp312-win32.whl", hash = "sha256:7df71011e51a7a24e63c2266e88cd3024e164ae2f218152d7b66ba6f45915380"}, + {file = "winrt_runtime-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:974909e6c0b785054077ff88af827f369641f17865833f592ce2017736e6669c"}, + {file = "winrt_runtime-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:1cbb43f3b06fb24b350805c73ef664b9a8a5b3d4987d15aa1c591f7ab6a54147"}, + {file = "winrt_runtime-3.1.0-cp313-cp313-win32.whl", hash = "sha256:7d92db2810c2d6c7148c3817b7d57fc2b3e6c433d644a2097485d4c6dfd92cad"}, + {file = "winrt_runtime-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:378bfded5b8c20b34c0d68be317e05031b37d1c168c50afca40e7af6863934e0"}, + {file = "winrt_runtime-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ca3f4c728973194c6b07a18be25ba6d3eee08e6bf5bd6eaec4dee482be30811c"}, + {file = "winrt_runtime-3.1.0-cp39-cp39-win32.whl", hash = "sha256:3d8c5de946a903fdb783645ef72842f53b469cd7c72127930c3239047ec4aaaa"}, + {file = "winrt_runtime-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cedd4cbcc54718348d02a8a1409ef374d4fdfa130bdc3a7f2da9bec66387527b"}, + {file = "winrt_runtime-3.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:975e8000643857b058a29c27b39cc9aa40a74f7f6287f196c1798113aaea088c"}, + {file = "winrt_runtime-3.1.0.tar.gz", hash = "sha256:943d01a960a3d3c55da8c98eeb66ec801e2306c17fe70eaad3b97b2fd248368b"}, ] +[package.dependencies] +typing_extensions = ">=4.12.2" + [[package]] name = "winrt-windows-devices-bluetooth" -version = "2.2.0" +version = "3.1.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false -python-versions = "<3.14,>=3.9" +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win32.whl", hash = "sha256:f3ced50ded44f74ac901d05f99cdd0bdf78e3a939a42d3cd80c33e510b4b8569"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:241a8f0ab06f6178d2e5757e7bc1f6c37e00e65ab6858ae676a1723a6445fa92"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3abefa3d11b4af9d9731d9d1a71083b1ef301fa30f7006a6c1f341426dd6d733"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4215c45595201f5f43f98b1e8911ff5cb0b303fe3298fa4d91a7bdc6d5523853"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cda69842b30bf56b10ea1a747d01b295abc910d9ccc10e9c97e8f554cd536e0"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7c12a28cd04eb05bacc73d8025ba135a929b9d511d21f20d0072d735853e8a2"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win32.whl", hash = "sha256:c929ea5215942fb26081b26aae094a2f70551cc0a59499ab2c9ea1f6d6b991f9"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1444e2031f3e69990d412b9edf75413a09280744bbc088a6b0760d94d356d4b"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f2d06ce6c43e37ea09ac073805ac6f9f62ae10ce552c90ae6eca978accd3f434"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win32.whl", hash = "sha256:b44a45c60f1d9fa288a12119991060ef7998793c6b93baa84308cfb090492788"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:fb698a55d06dc34643437b370c35fa064bd28762561e880715a30463c359fa44"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:cb350bfe21bab3573c9cd84006efad9c46a395a2943ab474105aed8b21bb88a4"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp39-cp39-win32.whl", hash = "sha256:7ee056e4c1a542352bcacbb95f898b7ae2739b3e0a63f7ab1290a7e2569f6393"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:f919cee2a49c3c48d1ef9dd84b419a6438000ef43bc35a7a349291c162cab4f3"}, - {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:f223af93675f6f92ab87de08c6d413ecc8ab19014b7438893437c42dcb2b0969"}, - {file = "winrt_windows_devices_bluetooth-2.2.0.tar.gz", hash = "sha256:95a5cf9c1e915557a28a4f017ea1ff7357039ee23526258f9cc161cf080b4577"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp310-cp310-win32.whl", hash = "sha256:94d0a618355e3b96580382002c730042c42408dc2dd8f4ebf199144a80a37df6"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b89ef0749ff372d7059b13edb1be4d239c992c8e5ff3691df04b0307b576c9e5"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:48a30974815f6a41ae80445e18b1704c832d05ebcdebb48b32b58fe9c8100d03"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp311-cp311-win32.whl", hash = "sha256:bca4c50e91c676a88fea1c0bc2e70aa47c3d6d4482a90453f142fc6b476eaf00"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc98be23c34e264e15379a1d7718c33d229e39c72ac9f8774ed95e1fcf3dd4d3"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:e27c99ce253d48e4b1f17369128499ad1fc8ccdb12c91786816f25951ef68a09"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp312-cp312-win32.whl", hash = "sha256:48a1b777dfec4a24ea9526f57a955c12b39bc9d3f5e337ea8d932e5f02b0c90c"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d34495199f40ab57b465952319f03d079572542cd65c25059ea8802fd510b02f"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:d466638be4ac8fe0668dc9db573446aa36ea078851490632e4dd49f7f2858da4"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp313-cp313-win32.whl", hash = "sha256:27e139c14961bc15c82679b468d7d064046039678b65d5683147fedbd33a5b65"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d6754fafdd0cfe149754a7201f0eee59ee772da948dccdef5f909e20545e9d"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:f2c01ea3d6cb306985089a962cfb23d799dd439cc57eef164ab4262486340e9c"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp39-cp39-win32.whl", hash = "sha256:67efb3078fc69cb79b09b6eb9b399c4d8059e81f7de60345f5b529b0353c569c"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:ec246db487ec58c576b6c544b92b2057e2e8152744e2c382c122724c02c71f10"}, + {file = "winrt_windows_devices_bluetooth-3.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:0bad67e3040ec2f51017db8e3b858325833fbb0138c70ada94cbd68f20bd0d2f"}, + {file = "winrt_windows_devices_bluetooth-3.1.0.tar.gz", hash = "sha256:8f05831e63557eabdd59175895c96671c078176417fd3c7f23157dfe398802f2"}, ] [package.dependencies] -winrt-runtime = "2.2.0" +winrt-runtime = "3.1.0" [package.extras] -all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (==2.2.0)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (==2.2.0)", "winrt-Windows.Devices.Enumeration[all] (==2.2.0)", "winrt-Windows.Devices.Radios[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Networking[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)"] +all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (==3.1.0)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (==3.1.0)", "winrt-Windows.Devices.Enumeration[all] (==3.1.0)", "winrt-Windows.Devices.Radios[all] (==3.1.0)", "winrt-Windows.Foundation.Collections[all] (==3.1.0)", "winrt-Windows.Foundation[all] (==3.1.0)", "winrt-Windows.Networking[all] (==3.1.0)", "winrt-Windows.Storage.Streams[all] (==3.1.0)"] [[package]] name = "winrt-windows-devices-bluetooth-advertisement" -version = "2.2.0" +version = "3.1.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false -python-versions = "<3.14,>=3.9" +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win32.whl", hash = "sha256:3d5fddffd5f6eeafebe1bcbaa096b8962c28c9236490f6f887ac2ed3ee4ed62c"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1cb5a835dc3574b0c47a613fa49eeeccdd9aa5801d43d7b7606ad5ce3614a54"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:9c2530c4972671ffb8a6e54621490c6c7a8c13b4d57e6474e05b62f211bbaab6"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win32.whl", hash = "sha256:28b36b3be137bdb6bdaad0d7a620c1a8b156e3c2737d08b9827af02b3c9d52bf"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:52948f17ecfc70c58b07077191985712172b518b5e3f4874e5708d175b7ace72"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:338296b76c01840c1dc10799a405b76460346bf677af11e6ab324311fd58e1a9"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win32.whl", hash = "sha256:4c14f48ac1886a3d374ee511467f0a61f26d88a321bf97d47429859730ee9248"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:89a658e901de88373e6a17a98273b8555e3f80563f2cc362b7f75817a7f9d915"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3b2b1b34f37a3329cf72793a089dd13fefd7b582c3e3a53a69a1353fd18940a3"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win32.whl", hash = "sha256:1b2d42c3d90b3e985954196b9a9e4007e22ff468d3d020c5a4acdee2821018fe"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d964c599670ea21b97afe2435e7638ca26e04936aacc0550474b6ec3fea988f"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:add4f459f0a02d1da38d579c3af887cfc3fe54f7782d779cf4ffe7f24404f1ff"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp39-cp39-win32.whl", hash = "sha256:756aeb2408bd59983a34da7f2552690d9e1071ad75de96aff15b365e1137b157"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9d19ef4cb00f58e10bdd0a2eb497eabecb3a2a5586fdcacebae6f0009585f3f1"}, - {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:1008641262bbbe130b6fcda76b9c890327aa416ef5b240a6a2cbb895d37dd3c7"}, - {file = "winrt_windows_devices_bluetooth_advertisement-2.2.0.tar.gz", hash = "sha256:bcbf246994b60e5de4bea9eb3fa01c5d6452200789004d14df70b27be9aa4775"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp310-cp310-win32.whl", hash = "sha256:5adfd0a2132333a71d0a3615925c21f89fdb5e13ffd9ccc629a055d3a321b784"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8a7bcc9fd7ba9a4cadc72a7c64c40916e7e1e970d7582e1cf7fdee9fbf8f9f08"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:97be2a6aabde1f4247c46d760f73d3d0ff73d3e2ad83c6bbf547ebfaf942de5d"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp311-cp311-win32.whl", hash = "sha256:9c96307d25b3493c26a7bae1820c840756277652756c60b221641f66ea0a53a7"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:41ab6448150c70c42d5ae617a1788a5b3370500cb69099191306c85c3c86ad48"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f3558cfa0d69f56b4bf115d6e4484be3e02f6ff4b5168d7437c3eb83f8f46cb3"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp312-cp312-win32.whl", hash = "sha256:16fa2615649244f94761e60088c9559adb000804f1058a0459fd8240f6cb16ee"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac2539c0b0b8236326f2718efb90bd7986c307a480e8c76409a78e2c71b78b88"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c4949a2667ac57bdf2ee5ed27e8abde1d768fc239a741de7b7f74d3b56faf935"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp313-cp313-win32.whl", hash = "sha256:6d9799174e359475f9addde8216d835e5e0f2298904e2c744706338cece6e64a"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:5142f5d983a685f06dafc63fe92f1b93d64275724866322bdd4223efb001dce8"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d126820df6c5cb66f6042751773343e7a585b771510e96f090fd5eebd428ecb"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp39-cp39-win32.whl", hash = "sha256:60b57f5b4ad51936b3ffb04e0200c01d780fe502cb4fc957d1ba956490bdcf54"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:482501b4679ceabbd3e5743b52e218edd62f9ad503872e845af3ebaa90f918b9"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:e2fa2974354536a21d94a030c3ea223e4fa35dabaaf539ec206c50bef71fdcfe"}, + {file = "winrt_windows_devices_bluetooth_advertisement-3.1.0.tar.gz", hash = "sha256:97a6869e415d2834230b7738073bae20ccc0742863a03bf977062bce55304856"}, ] [package.dependencies] -winrt-runtime = "2.2.0" +winrt-runtime = "3.1.0" [package.extras] -all = ["winrt-Windows.Devices.Bluetooth[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)"] +all = ["winrt-Windows.Devices.Bluetooth[all] (==3.1.0)", "winrt-Windows.Foundation.Collections[all] (==3.1.0)", "winrt-Windows.Foundation[all] (==3.1.0)", "winrt-Windows.Storage.Streams[all] (==3.1.0)"] [[package]] name = "winrt-windows-devices-bluetooth-genericattributeprofile" -version = "2.2.0" +version = "3.1.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false -python-versions = "<3.14,>=3.9" +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win32.whl", hash = "sha256:1472f89b9d6527137e1c58dfb46f22faf2753c477a9d4f85f789b3266ad282a9"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:e25702f1aa6d4ecdf335805a50048e70ee2206499cfd7ed4fbe1a92358bdcc16"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:d07d27a6f8f7a1f52aa978724d5a09d43053b428c71563892b70df409049a37a"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win32.whl", hash = "sha256:5c6c863daaa99b0bb670730296137b7c718d94726c112ff44ec73c8b27a12ded"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbee7c90c0a155477eba09eb09297711b2cb32f6ede4c01d0afe58cb3776f06a"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:655777193fd338e1a8c30ebbb8460c017d08548c54ddec9fc5503f1605c47332"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win32.whl", hash = "sha256:45a48ab8da94eee1590f22826c084f4b1f8c32107a023f05d6a03437931a6852"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:395cb2fecd0835a402c3c4f274395bc689549b2a6b4155d3ad97b29ec87ee4f2"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:25063b43550c5630f188cfb263ab09acc920db97d1625c48e24baa6e7d445b6e"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win32.whl", hash = "sha256:d1d26512fe45c3be0dbeb932dbd75abd580cd46ccfc278fcf51042eff302fa9c"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:21786840502a34958dd5fb137381f9144a6437b49ee90a877beb3148ead6cfe9"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d98852458b639e875bb4895a9ad2d5626059bc99c5f745be0560d235502d648"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp39-cp39-win32.whl", hash = "sha256:827b390b1a47c9aa6bfd717b66822f4fc698b0c02c8678924e2bc6ac37093b65"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:727567b725ca94b677bda97a6f725d58fc1a4652d4cc232b44cc57dd7ba9ee87"}, - {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:ac901d17d2350785bce18282cd29d002d2c4da8adff5160891c4115ae010a2d0"}, - {file = "winrt_windows_devices_bluetooth_genericattributeprofile-2.2.0.tar.gz", hash = "sha256:0de4ee5f57223107f25c20f6bb2739947670a2f8cf09907f3e611efc81e7c6e0"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp310-cp310-win32.whl", hash = "sha256:61d503f682e2834f1357114f73c5351b3151e6d54dc11d55e65c509ad574a562"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:e14ddfdee9a7afdf3d47760e09d43e652d3dc7684623ba0149d0fdd0bf118548"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a23554705742576fa30022a6bd6f973bf53d0042a9a47925535ec839776dbfc7"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp311-cp311-win32.whl", hash = "sha256:094433e89cc7beb10a5099a446e8c98dba1aa8238c128844b42d210810d9b95e"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:c252bb7bd635035595a249a897045a6976751f60ebf8a88641c0d8f82e11ee06"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19ca04aa72e785d91c802c40d586eac0f9ae0cc70c7b68af0ce18e837426d55f"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp312-cp312-win32.whl", hash = "sha256:8382c8b3b2597ca6e09b4ca879e38e92d766075a43662138991e478bdd8bc152"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a0cd28b491ecb1b7502bab327217367b74f2e51081da6548d182d950f5a190f"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:885bb29ad1b273ba39e6ae0076f5fb8ac4aca45329ab63ae84f84512fe34ee2a"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp313-cp313-win32.whl", hash = "sha256:0d0289e10951f533164da5b03f902eff0d1ec82a34df0c75447860aefae7a745"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:5f3069684768eeabfad92bebb403306838822925b1cd364086b36af6165ef7fb"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:a31886a637f6c3110ea0e80699f7d344d421845ac720e133e85ffe34940883bb"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp39-cp39-win32.whl", hash = "sha256:ca44727e11f4ee34f5b011c8567f75880bf4a409b047f16cbaeddd6d06119987"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0a74778a24f26bc4b2abc8e59e75c4fa21e7968b3257a3d7db35dc92fc2d5e2d"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:d045f6130878895ce51b3e58baa38ff8644e185cade2f341080b6bf871978533"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.1.0.tar.gz", hash = "sha256:98aba7245439db7b76fd33ee8034d96c3889bc4be31ef885eaa453b60b6bb277"}, ] [package.dependencies] -winrt-runtime = "2.2.0" +winrt-runtime = "3.1.0" [package.extras] -all = ["winrt-Windows.Devices.Bluetooth[all] (==2.2.0)", "winrt-Windows.Devices.Enumeration[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)"] +all = ["winrt-Windows.Devices.Bluetooth[all] (==3.1.0)", "winrt-Windows.Devices.Enumeration[all] (==3.1.0)", "winrt-Windows.Foundation.Collections[all] (==3.1.0)", "winrt-Windows.Foundation[all] (==3.1.0)", "winrt-Windows.Storage.Streams[all] (==3.1.0)"] [[package]] name = "winrt-windows-devices-enumeration" -version = "2.2.0" +version = "3.1.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false -python-versions = "<3.14,>=3.9" +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win32.whl", hash = "sha256:69e87ba0ae5c31f60bc07d0558d91af96213d8b8b2b1be0ccf3e5824cab466ef"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6993d5305ff750c5c51f57253935458996fb45c049891f2fb00772cc6ece6b3"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bb54aa94b17052d65fe4fa5777183cf9bfb697574c3461759114d3ec0c802cec"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win32.whl", hash = "sha256:fef83263e73c2611d223f06735d2c2a16629d723f74e1964dc882f90b6e1cda1"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:cf3cec5a6fba069ecbd4f3efa95e9f197aeebdd05a60bcd52b953888169ab7ee"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:d9ce308c492c1e9f2417f91ad02e366f4269cc1c6d271f0be4092b758df4c9bf"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win32.whl", hash = "sha256:5bea21988749fad21574ea789b4090cfbfbb982a5f9a42b2d6f05b3ad47f68bd"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c9718d7033550a029e0c2848ff620bf063a519cb22ab9d880d64ceb302763a48"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:69f67f01aa519304e4af04a1a23261bd8b57136395de2e08d56968f9c6daa18e"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win32.whl", hash = "sha256:84447916282773d7b7e5a445eae0ab273c21105f1bbcdfb7d8e21cd41403d5c1"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:1bb9d97f8d2518bb5b331f825431814277de4341811a1776e79d51767e79700c"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:2a5408423f680f6b36d7accad7151336ea16ad1eaa2652f60ed88e2cbd14562c"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp39-cp39-win32.whl", hash = "sha256:51f4c9b6f3376913e3009bfe232cfc082357b24d6eeec098cf53f361527e1c1f"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e6895d5538539d0c6bd081374e7646684901038d4d2dede7841b63adfaf8086"}, - {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0845fca0841003ae446650ab6695c38d45623bc1e8e40a43e839e450a874fd6f"}, - {file = "winrt_windows_devices_enumeration-2.2.0.tar.gz", hash = "sha256:cfe1780101e3ef9c5b4716cca608aa6b6ddf19f1d7a2a70434241d438db19d3d"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp310-cp310-win32.whl", hash = "sha256:147c36b9dedcdfaf1f9739a10eb32a87478a4904c15f9fba38d6b57aef5eec71"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0c6df0eefeeaf0e7cf0b261410eb35480cfc049d1eae74bc44bceaf7b11d9b2"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:e3917892e26f64a8e51b9c7b1a87f5199341abbe430df0962af0563b7fd7b133"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp311-cp311-win32.whl", hash = "sha256:a7985b5a8707ab5ba36519546167ea278e5efe325782760b13fbe6d80bf4332e"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:5d5e74935329fb97bf6bb4db32d687756ed57e1ffe825a25012768a38619fa3c"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:65611c00dfc48d629449f0a29ae5b216fd41c6aec788750fac42047362b5a75a"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp312-cp312-win32.whl", hash = "sha256:45be0323c3314f89c98cd0af5637ae8f37c6e977c079491de170f21951147c9d"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:168a2fc407e24f5530e7eaa2ab93ad06e48e34f05308a5f63d2f251110ca24c8"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:f2cc0e9e57c6cb0df41c95483b2f37eae38a85e2c65a7eb1f31d2499813af244"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp313-cp313-win32.whl", hash = "sha256:5ffad5bac3786c1ba22705a58a1eb15abcc81c36552a9be4c29c24a7b81d7a29"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:15bba8f1d86134733bb6feb8c18cff75eff96c0d718f998ad43f73bb0a23b20e"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2a09ba12cc6639493f5e8e3a918e8ae5ca2ede2bde5c89d92aa2268191094842"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp39-cp39-win32.whl", hash = "sha256:af64131e7736b8341f07fef566ed951c32b602b940eb905b6a488048609d61db"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3c6a87097f7997185c3ecb2c650151b9f796697586a91ee213721dc0fbaf9e2d"}, + {file = "winrt_windows_devices_enumeration-3.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:986a70ebee923f272f7e5c6ad9c265977861d139e8ebae0d6043f585ec959208"}, + {file = "winrt_windows_devices_enumeration-3.1.0.tar.gz", hash = "sha256:0a92505e56322be58e69278077146bb11d80e373a091e511c24d2cb236eea18e"}, ] [package.dependencies] -winrt-runtime = "2.2.0" +winrt-runtime = "3.1.0" [package.extras] -all = ["winrt-Windows.ApplicationModel.Background[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Security.Credentials[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)", "winrt-Windows.UI.Popups[all] (==2.2.0)", "winrt-Windows.UI[all] (==2.2.0)"] +all = ["winrt-Windows.ApplicationModel.Background[all] (==3.1.0)", "winrt-Windows.Foundation.Collections[all] (==3.1.0)", "winrt-Windows.Foundation[all] (==3.1.0)", "winrt-Windows.Security.Credentials[all] (==3.1.0)", "winrt-Windows.Storage.Streams[all] (==3.1.0)", "winrt-Windows.UI.Popups[all] (==3.1.0)", "winrt-Windows.UI[all] (==3.1.0)"] [[package]] name = "winrt-windows-foundation" -version = "2.2.0" +version = "3.1.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false -python-versions = "<3.14,>=3.9" +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ - {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win32.whl", hash = "sha256:cb86bbf04f72d983e4ae13db0a48784638b36214bb2c44809f39686ef3314354"}, - {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2dbd0957216c07db4b91a144a0ffa7c8892cc668b19ca15b78067255445741b2"}, - {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:5345f7d0504aa1a605be5b5fe0d1944b322591f7669c2c86b7c45384924c8c9b"}, - {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win32.whl", hash = "sha256:f6711adf8a34e48c94183e792f153de5f3796f8f3c045356544605384bbcb7e1"}, - {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:0a5bfe2647659e7ec288d8552e61e577a931914531ccc9cb958469d85f049d6b"}, - {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9eabbd1b179fd04f167884fa0feaa17ccd67d89f6eac4099b16c6c0dc22e9f32"}, - {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win32.whl", hash = "sha256:0f0319659f00d04d13fc5db45f574479a396147c955628dc2dda056397a0df28"}, - {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:8bc605242d268cd8ccce68c78ec4a967b8e5431c3a969c9e7a01d454696dfb3f"}, - {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f901b20c3a874a2cf9dcb1e97bbcff329d95fd3859a873be314a5a58073b4690"}, - {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win32.whl", hash = "sha256:c5cf43bb1dccf3a302d16572d53f26479d277e02606531782c364056c2323678"}, - {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:10c84276ff182a06da6deb1ba9ad375f9b3fbc15c3684a160e775005d915197a"}, - {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:473cc57664bfd5401ec171c8f55079cdc8a980210f2c82fb2945361ea640bfbf"}, - {file = "winrt_Windows.Foundation-2.2.0-cp39-cp39-win32.whl", hash = "sha256:32578bd31eda714bc5cb5b10f0e778c720a2e45bc9b3c60690faa1615336047d"}, - {file = "winrt_Windows.Foundation-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bfb62127959f56fdacad6a817176a8b22cf6917a0d5c3e5d25cdad33a90173a"}, - {file = "winrt_Windows.Foundation-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:07ea5a2f05cb9fb433371e55f70fbe27f32a6eb07ae28042f01678b4d82d823a"}, - {file = "winrt_windows_foundation-2.2.0.tar.gz", hash = "sha256:9a76291204900cd92008163fbe273ae43c9a925ca4a5a29cdd736e59cd397bf1"}, + {file = "winrt_windows_foundation-3.1.0-cp310-cp310-win32.whl", hash = "sha256:08bfb0411c9503573437da21a2cd732f634137a37837d8b6cba5393614b1e332"}, + {file = "winrt_windows_foundation-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:156b1befb83b8ae8dec504318af75982b565e9d9618396bcb967a44daed6ae78"}, + {file = "winrt_windows_foundation-3.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:92a8655feea4d2e3279536eb353175fc9172a507f7095cf5cd3cf6187abb0ec5"}, + {file = "winrt_windows_foundation-3.1.0-cp311-cp311-win32.whl", hash = "sha256:3e062b6dfbfc9a2c3ac6f09fd8dc4c0058d258d6d85aec6b588d9f75aa4133ed"}, + {file = "winrt_windows_foundation-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fe52ae88265f7bd8853c3ed3801704635702bd8cd084c758c6e5ae303bb5a7ca"}, + {file = "winrt_windows_foundation-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:183f8aba1cfe06b1ab63aeae7f1bf91055519c096b5df48e6d0fe82f871645d7"}, + {file = "winrt_windows_foundation-3.1.0-cp312-cp312-win32.whl", hash = "sha256:c70ec14daacd121dcddea1b94e1b81671a7cccccf4f18bdf57c79217948e7fc2"}, + {file = "winrt_windows_foundation-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee5fd8546a88cb68960ae6ced056af0058212884f7a0f1fde9e3b53a2d1c1030"}, + {file = "winrt_windows_foundation-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:86931ca0a7037799a0719d3975c78477195e11f9dfff1b152bb2efd76d7b99d7"}, + {file = "winrt_windows_foundation-3.1.0-cp313-cp313-win32.whl", hash = "sha256:0a7770d76e0dfd00092c1a3a3ac33455e6aa4fd73b51fd16e787aa1202f31bef"}, + {file = "winrt_windows_foundation-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:395fd880941ce04e2e089e02452e6e6c4c0b991ba1f5f1f4bbad50c2d30915f5"}, + {file = "winrt_windows_foundation-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:c89b2076117de98848f5356fc16a97444eb0bcabbf2cae12534f6d40c2d6d6e0"}, + {file = "winrt_windows_foundation-3.1.0-cp39-cp39-win32.whl", hash = "sha256:8764d4df3bc867ff27a125868d29aea2c52c739b0c3b3d55b81dd7fc1c9faff3"}, + {file = "winrt_windows_foundation-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:d402081cdab706670af8007e80d12d5c6b37a1c099aa6e976c8cca74fac96fb1"}, + {file = "winrt_windows_foundation-3.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:ac061519da737221f51fbdea6423fe59c128b76290fc34cba4880e3bbe97127d"}, + {file = "winrt_windows_foundation-3.1.0.tar.gz", hash = "sha256:3a2f9243b76fe093c74f8aad8cc3c7c93c6ec2a9498817058d85de0a2e9b083d"}, ] [package.dependencies] -winrt-runtime = "2.2.0" +winrt-runtime = "3.1.0" [package.extras] -all = ["winrt-Windows.Foundation.Collections[all] (==2.2.0)"] +all = ["winrt-Windows.Foundation.Collections[all] (==3.1.0)"] [[package]] name = "winrt-windows-foundation-collections" -version = "2.2.0" +version = "3.1.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false -python-versions = "<3.14,>=3.9" +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win32.whl", hash = "sha256:92a031fca53910c8bce683391888ba3427db178fc47653310de16fb7e9131e9d"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:a71925d738a443cf27522f34ced84730f1b325f69ccdd0145580e6078d4481c5"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:74c9419b26b510e6e95182e02dc55a78094b6f2af5002330467d030ae6d0b765"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win32.whl", hash = "sha256:8a76d79be0af1840b9c5ac1879dcf5aa65b512accd8278ac6424dcbfdb2a6fe1"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:b18dcd7bc8cf70758b965397e26da725ac345dd9f16b922b0204e8f21ed4d7e6"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d6b0b04683e98989dd611940b5fe36c1338f6d91f43c1bdc88f2f2f1956a968"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win32.whl", hash = "sha256:ade4ea4584ba96e39d2b34f1036d8cb40ff2e9609a090562cfd2b8837dc7f828"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:1e896291c5efe0566db84eab13888bee7300392a6811ae85c55ced51bac0b147"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:e44e13027597fcc638073459dcc159a21c57f9dbe0e9a2282326e32386c25bd0"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win32.whl", hash = "sha256:ea7fa3a7ecb754eb09408e7127cd960d316cc1ba60a6440e191a81f14b42265c"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:f338860e27a8a67b386273c73ad10c680a9f40a42e0185cc6443d208a7425ece"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:dd705d4c62bd8c109f2bc667a0c76dc30ef9a1b2ced3e7bd95253a31e39781df"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp39-cp39-win32.whl", hash = "sha256:6798595621ad58473fe9e86f5f58d732628d88f06535b68c4d86cb5aed78f2b3"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c8ac098a60dad586e950a8236bab09ae57b6a08147d36db6b0aed135a9a81831"}, - {file = "winrt_Windows.Foundation.Collections-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:c67105ebd88faf10d2941516c0ea9f73d9282fb8a7d2a73163a7a7e013bba839"}, - {file = "winrt_windows_foundation_collections-2.2.0.tar.gz", hash = "sha256:10db64da49185af3e14465cd65ec4055eb122a96daedb73b774889f3b7fcfa63"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp310-cp310-win32.whl", hash = "sha256:240f37ea59b90645fc910de5b0af495a40a8d1a03fbc01cb5617f0208a028dc7"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d55c56dea94c63c387f4895e9dc1c5fb4713c985b1fde0df8f6888fefb0f106b"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:fca3e259ae868c57ead51df5d9da2e7dfe938637483e67dfc6d7bfdc80e0c13b"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp311-cp311-win32.whl", hash = "sha256:cd0c1c19c76d15335f51524015c37d4b9766a78888c23ef94462c4e576efd553"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:7c6f692bbc86428871d8e18527df873bd3edf8803ce8d6e8bfa3fa0f14276db5"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:adba015d8d2aa67f53a2d52f408a91ed79f4473375b21143d71ceeb9a76f403e"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp312-cp312-win32.whl", hash = "sha256:4816b260f06ec17b7edd749173291997cb3f418186b135b62b581c487acd22eb"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1762ea1808ebce15ef3509905a07bf6c64b0647e9bfa4edcefc8d7459193c558"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4bb0e32973b63ea19b59c83a7a2200e6dd2a9607c61a907b77e24f963c240914"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp313-cp313-win32.whl", hash = "sha256:131a7a4ddde1699ea80d0f7da1610b0aaec719c46bbdbd2bd489d2cd8c7ba7e8"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee297077a573bc6ad17b731b326db1bf5818caa7d724b70104fe6bceceaeea12"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:cd95c677dec999327627a9f324d9ee975f9e224107e74c3902aa83d06ca68a86"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp39-cp39-win32.whl", hash = "sha256:edc32e28d7c3b421d2f77278d5504fee9ad7577660d119feec70b7ef59a9c29c"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:f6882b58b04e41310828566ca7f07253dfb4eb87326553936885f9f3b23394da"}, + {file = "winrt_windows_foundation_collections-3.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:d47918c79719e7776298453f27072a10286028cfb359a4b6f3f697d1e6affab6"}, + {file = "winrt_windows_foundation_collections-3.1.0.tar.gz", hash = "sha256:7ab521a42031f8f16898460f2b8efc151c1b7e88ccfb164f6412cce03af533a5"}, ] [package.dependencies] -winrt-runtime = "2.2.0" +winrt-runtime = "3.1.0" [package.extras] -all = ["winrt-Windows.Foundation[all] (==2.2.0)"] +all = ["winrt-Windows.Foundation[all] (==3.1.0)"] [[package]] name = "winrt-windows-storage-streams" -version = "2.2.0" +version = "3.1.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false -python-versions = "<3.14,>=3.9" +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ - {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win32.whl", hash = "sha256:e888ae08f1245f8b6d53783487581fc664683bb29778f2acca6bafb6a78bcc22"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9213576d566398657142372aa34354b9f7b8ce0581cff308c7afbc0d908368a1"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:49d2bdd749994fb81c813f02f3c506fff580f358083b65a123308f322c2fe6cf"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win32.whl", hash = "sha256:db4ebe7ed79a585a1bb78a3f8cea05f7d74a6a8bc913f61b31ddfe3ae10d134d"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9f77c5398eb90c58645c62b6f278f701d2636c0007817cc6fc28256adbebdcb"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:894c2616eeae887275a1a64a4233964f9466ee1281b8c11ec7c06d64aafec88a"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win32.whl", hash = "sha256:85a2eefb2935db92d10b8e9be836c431d47298b566b55da633b11f822c63838d"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f88cdc6204219c7f1b58d793826ea2eff013a45306fbb340d61c10896c237547"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:78af200d0db5ebe151b1df194de97f1e71c2d5f5cba4da09798c15402f4ab91d"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win32.whl", hash = "sha256:6408184ba5d17e0d408d7c0b85357a58f13c775521d17a8730f1a680553e0061"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:ad9cd8e97cf4115ba074ec153ab273c370e690abb010d8b3b970339d20f94321"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c467cf04005b72efd769ea99c7c15973db44d5ac6084a7c7714af85e49981abd"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp39-cp39-win32.whl", hash = "sha256:f72559b5de7c3a0cab97cd50ab594a0e3278df4d38e03f79b5b2d2e13e926c4c"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:37bf5bb801aa1e4a4c6f3ddfe2b8c9b05d7726ebfdfc8b9bfe41bdcc3866749b"}, - {file = "winrt_Windows.Storage.Streams-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:2dcab77a7affb1136503edec82a755b82716abd882fadd5f50ce260438b9c21b"}, - {file = "winrt_windows_storage_streams-2.2.0.tar.gz", hash = "sha256:46a8718c4e00a129d305f03571789f4bed530c05e135c2476494af93f374b68a"}, + {file = "winrt_windows_storage_streams-3.1.0-cp310-cp310-win32.whl", hash = "sha256:41e862047e63b3f0db316636e4e68a567eb47e32c564aef10b249df7ba2628ba"}, + {file = "winrt_windows_storage_streams-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1ab8698f9264a96aead8d58c99b37946f0187f6bf620f08a8d426554131a898d"}, + {file = "winrt_windows_storage_streams-3.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:50bb92d0787211270139f9830cc8b9321300735623dc1d53c589f26813312fe1"}, + {file = "winrt_windows_storage_streams-3.1.0-cp311-cp311-win32.whl", hash = "sha256:a3fa73ea35b54405fb623ca00b26605c3c407a340fd184afaa41893f3d1ff0e8"}, + {file = "winrt_windows_storage_streams-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ab268b22c7be21ae58d251811c006ccbb2e48f1711c8052893b356001ca7135f"}, + {file = "winrt_windows_storage_streams-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa27d8973c02e4260943a77dc44bc2693af214bb74dd2d1c57a216fa320c24f0"}, + {file = "winrt_windows_storage_streams-3.1.0-cp312-cp312-win32.whl", hash = "sha256:7efe54a22182be2c3e14c336507ddeb9c1bc62f36efbcc03ca4e51ea4c36666c"}, + {file = "winrt_windows_storage_streams-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:62dc488b49a014210bc57b9ebd531f5dfee4782e5a168677f27101d1f9d8473f"}, + {file = "winrt_windows_storage_streams-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:889de8d4c4f80b32ddd3ab924162d1fb38c8eb6469ff4292a38f8790cfa7096c"}, + {file = "winrt_windows_storage_streams-3.1.0-cp313-cp313-win32.whl", hash = "sha256:ec2de832588a3caaf71b9ece120c8583865ed5f6eeb0282496392f4aa570c5e7"}, + {file = "winrt_windows_storage_streams-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:be147a404dabc163e7b6a2bf270d362b8530cc995861a0211d99750ca17988fd"}, + {file = "winrt_windows_storage_streams-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:3f5205f7560795746ee741c6e7a55d679e63e29499aca14a9785ce325cae0279"}, + {file = "winrt_windows_storage_streams-3.1.0-cp39-cp39-win32.whl", hash = "sha256:debedf169a393abbd5e1e31ad5b4520bdd861d750bd73447b24e179a49ce1a9b"}, + {file = "winrt_windows_storage_streams-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bdd65c0e46acf596d8732b61c27039a3781980696968c4c8edc09681d2cf10c5"}, + {file = "winrt_windows_storage_streams-3.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:279c18fac812378961c2b1750ba7f355c08255031cabc9352f8450f812ad731e"}, + {file = "winrt_windows_storage_streams-3.1.0.tar.gz", hash = "sha256:3c0faca60b14fc5a459e546cdc1bae9a19d7939cb0d57032f945b392163ee6ea"}, ] [package.dependencies] -winrt-runtime = "2.2.0" - -[package.extras] -all = ["winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Storage[all] (==2.2.0)", "winrt-Windows.System[all] (==2.2.0)"] - -[[package]] -name = "zipp" -version = "3.19.1" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, - {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, -] +winrt-runtime = "3.1.0" [package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +all = ["winrt-Windows.Foundation.Collections[all] (==3.1.0)", "winrt-Windows.Foundation[all] (==3.1.0)", "winrt-Windows.Storage[all] (==3.1.0)", "winrt-Windows.System[all] (==3.1.0)"] [metadata] -lock-version = "2.0" -python-versions = ">=3.8,<3.14" -content-hash = "22885f16b59e72fb2bcf6fd88c81cabc7d48ebb05555f12375226200ca26c61e" +lock-version = "2.1" +python-versions = ">=3.9" +content-hash = "26e51524733733c0430cebba61666dcf25bbd138ba4269a69fad6de67a41e374" diff --git a/pyproject.toml b/pyproject.toml index 19559eefd..63c530833 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,21 @@ -[tool.poetry] +[project] name = "bleak" -version = "0.22.3" +version = "1.0.1" description = "Bluetooth Low Energy platform Agnostic Klient" -authors = ["Henrik Blidh "] +authors = [{ name = "Henrik Blidh", email = "henrik.blidh@nedomkull.com" }] license = "MIT" readme = "README.rst" -homepage = "https://github.com/hbldh/bleak" -documentation = "https://bleak.readthedocs.io" +requires-python = ">=3.9" +dynamic = ["classifiers", "dependencies"] + +[project.urls] +"Homepage" = "https://github.com/hbldh/bleak" +"Documentation" = "https://bleak.readthedocs.io" +"Changelog" = "https://github.com/hbldh/bleak/blob/develop/CHANGELOG.rst" +"Support" = "https://github.com/hbldh/bleak/discussions" +"Issues" = "https://github.com/hbldh/bleak/issues" + +[tool.poetry] classifiers = [ "Development Status :: 4 - Beta", "Framework :: AsyncIO", @@ -16,37 +25,32 @@ classifiers = [ "Operating System :: Android", ] -[tool.poetry.urls] -"Changelog" = "https://github.com/hbldh/bleak/blob/develop/CHANGELOG.rst" -"Support" = "https://github.com/hbldh/bleak/discussions" -"Issues" = "https://github.com/hbldh/bleak/issues" - [tool.poetry.dependencies] -python = ">=3.8,<3.14" -async-timeout = { version = ">= 3.0.0, < 5", python = "<3.11" } +async-timeout = { version = ">=3.0.0", python = "<3.11" } typing-extensions = { version = ">=4.7.0", python = "<3.12" } -pyobjc-core = { version = "^10.3", markers = "platform_system=='Darwin'" } -pyobjc-framework-CoreBluetooth = { version = "^10.3", markers = "platform_system=='Darwin'" } -pyobjc-framework-libdispatch = { version = "^10.3", markers = "platform_system=='Darwin'" } -bleak-winrt = { version = "^1.2.0", markers = "platform_system=='Windows'", python = "<3.12" } -"winrt-runtime" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } -"winrt-Windows.Devices.Bluetooth" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } -"winrt-Windows.Devices.Bluetooth.Advertisement" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } -"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } -"winrt-Windows.Devices.Enumeration" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } -"winrt-Windows.Foundation" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } -"winrt-Windows.Foundation.Collections" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } -"winrt-Windows.Storage.Streams" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } -dbus-fast = { version = ">=1.83.0, < 3", markers = "platform_system == 'Linux'" } +pyobjc-core = { version = ">=10.3", markers = "platform_system=='Darwin'" } +pyobjc-framework-CoreBluetooth = { version = ">=10.3", markers = "platform_system=='Darwin'" } +pyobjc-framework-libdispatch = { version = ">=10.3", markers = "platform_system=='Darwin'" } +"winrt-runtime" = { version = ">=3.1", markers = "platform_system=='Windows'" } +"winrt-Windows.Devices.Bluetooth" = { version = ">=3.1", markers = "platform_system=='Windows'" } +"winrt-Windows.Devices.Bluetooth.Advertisement" = { version = ">=3.1", markers = "platform_system=='Windows'" } +"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = { version = ">=3.1", markers = "platform_system=='Windows'" } +"winrt-Windows.Devices.Enumeration" = { version = ">=3.1", markers = "platform_system=='Windows'" } +"winrt-Windows.Foundation" = { version = ">=3.1", markers = "platform_system=='Windows'" } +"winrt-Windows.Foundation.Collections" = { version = ">=3.1", markers = "platform_system=='Windows'" } +"winrt-Windows.Storage.Streams" = { version = ">=3.1", markers = "platform_system=='Windows'" } +dbus-fast = { version = ">=1.83.0", markers = "platform_system == 'Linux'" } + +[tool.poetry.group.docs] +optional = true [tool.poetry.group.docs.dependencies] -Sphinx = "^5.1.1" -sphinx-rtd-theme = "^1.0.0" -tomli = "^2.0.1" +Sphinx = { version = ">=8.2.3", python = ">=3.11" } +sphinx-rtd-theme = { version = ">=3.0.2", python = ">=3.11" } [tool.poetry.group.lint.dependencies] black = ">=24.3,<25.0" -flake8 = "^5.0.0" +flake8 = "^7.1.1" isort = "^5.13.2" [tool.poetry.group.test.dependencies] @@ -55,11 +59,15 @@ pytest-asyncio = "^0.23.7" pytest-cov = "^3.0.0 " [build-system] -requires = ["poetry-core"] +requires = ["poetry-core>=2.0.0"] build-backend = "poetry.core.masonry.api" [tool.isort] profile = "black" -py_version=38 -src_paths = ["bleak", "examples", "tests", "typings"] -extend_skip = [".buildozer"] +py_version=39 +src_paths = ["bleak", "examples", "tests"] +extend_skip = [".buildozer", "docs"] + +[tool.mypy] +python_version = "3.9" +disable_error_code = ["import-not-found"] diff --git a/setup.cfg b/setup.cfg index 2a5d6a437..a13ac9540 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [flake8] exclude = docs,.venv,*.pyi,.buildozer,build,dist,.eggs -ignore = E203,E501,E704,W503 +ignore = E203,E231,E501,E704,W503 [aliases] test = pytest diff --git a/tests/bleak/backends/bluezdbus/test_deprecated_imports.py b/tests/bleak/backends/bluezdbus/test_deprecated_imports.py new file mode 100644 index 000000000..666075762 --- /dev/null +++ b/tests/bleak/backends/bluezdbus/test_deprecated_imports.py @@ -0,0 +1,55 @@ +import sys + +import pytest + +# isort: off + +if not sys.platform.startswith("linux"): + pytest.skip("backend only available on Linux", allow_module_level=True) + + +def test_deprecated_OrPattern_import(): + with pytest.warns( + DeprecationWarning, + match="importing OrPattern from bleak.backends.bluezdbus.advertisement_monitor is deprecated", + ) as recorder: + from bleak.backends.bluezdbus.advertisement_monitor import ( # noqa: F401 + OrPattern, # type: ignore[unused-import] + ) + + assert recorder.list[0].filename == __file__ + + +def test_deprecated_OrPatternLike_import(): + with pytest.warns( + DeprecationWarning, + match="importing OrPatternLike from bleak.backends.bluezdbus.advertisement_monitor is deprecated", + ) as recorder: + from bleak.backends.bluezdbus.advertisement_monitor import ( # noqa: F401 + OrPatternLike, # type: ignore[unused-import] + ) + + assert recorder.list[0].filename == __file__ + + +def test_deprecated_BlueZDiscoveryFilters_import(): + with pytest.warns( + DeprecationWarning, + match="importing BlueZDiscoveryFilters from bleak.backends.bluezdbus.scanner is deprecated", + ) as recorder: + from bleak.backends.bluezdbus.scanner import ( # noqa: F401 + BlueZDiscoveryFilters, # type: ignore[unused-import] + ) + assert recorder.list[0].filename == __file__ + + +def test_deprecated_BlueZScannerArgs_import(): + with pytest.warns( + DeprecationWarning, + match="importing BlueZScannerArgs from bleak.backends.bluezdbus.scanner is deprecated", + ) as recorder: + from bleak.backends.bluezdbus.scanner import ( # noqa: F401 + BlueZScannerArgs, # type: ignore[unused-import] + ) + + assert recorder.list[0].filename == __file__ diff --git a/tests/bleak/backends/bluezdbus/test_utils.py b/tests/bleak/backends/bluezdbus/test_utils.py index 5e7284336..d098c6060 100644 --- a/tests/bleak/backends/bluezdbus/test_utils.py +++ b/tests/bleak/backends/bluezdbus/test_utils.py @@ -22,3 +22,10 @@ def test_device_path_from_characteristic_path(): ) == "/org/bluez/hci0/dev_11_22_33_44_55_66" ) + + assert ( + device_path_from_characteristic_path( + "/org/bluez/hci10/dev_11_22_33_44_55_66/service000c/char000d" + ) + == "/org/bluez/hci10/dev_11_22_33_44_55_66" + ) diff --git a/tests/bleak/backends/bluezdbus/test_version.py b/tests/bleak/backends/bluezdbus/test_version.py index 6f36b7f54..3c855f984 100644 --- a/tests/bleak/backends/bluezdbus/test_version.py +++ b/tests/bleak/backends/bluezdbus/test_version.py @@ -11,22 +11,15 @@ @pytest.mark.asyncio @pytest.mark.parametrize( - "version,can_write_without_response,write_without_response_workaround_needed,hides_battery_characteristic,hides_device_name_characteristic", + "version", [ - (b"bluetoothctl: 5.34", False, False, False, False), - (b"bluetoothctl: 5.46", True, False, False, False), - (b"bluetoothctl: 5.48", True, False, True, True), - (b"bluetoothctl: 5.51", True, True, True, True), - (b"bluetoothctl: 5.63", True, True, False, True), - (b"", True, True, False, True), + (b"bluetoothctl: 5.51"), + (b"bluetoothctl: 5.63"), + (b""), ], ) async def test_bluez_version( version, - can_write_without_response, - write_without_response_workaround_needed, - hides_battery_characteristic, - hides_device_name_characteristic, ): """Test we can determine supported feature from bluetoothctl.""" mock_proc = Mock( @@ -39,16 +32,6 @@ async def test_bluez_version( BlueZFeatures._check_bluez_event = None await BlueZFeatures.check_bluez_version() assert BlueZFeatures.checked_bluez_version is True - assert BlueZFeatures.can_write_without_response == can_write_without_response - assert ( - not BlueZFeatures.write_without_response_workaround_needed - == write_without_response_workaround_needed - ) - assert BlueZFeatures.hides_battery_characteristic == hides_battery_characteristic - assert ( - BlueZFeatures.hides_device_name_characteristic - == hides_device_name_characteristic - ) @pytest.mark.asyncio diff --git a/tests/bleak/backends/winrt/test_deprecated_imports.py b/tests/bleak/backends/winrt/test_deprecated_imports.py new file mode 100644 index 000000000..9273aa444 --- /dev/null +++ b/tests/bleak/backends/winrt/test_deprecated_imports.py @@ -0,0 +1,20 @@ +import sys + +import pytest + +# isort: off + +if not sys.platform.startswith("win"): + pytest.skip("backend only available on windows", allow_module_level=True) + + +def test_deprecated_WinRTClientArgs_import(): + with pytest.warns( + DeprecationWarning, + match="importing WinRTClientArgs from bleak.backends.winrt.client is deprecated, use bleak.args.winrt instead", + ) as recorder: + from bleak.backends.winrt.client import ( # noqa: F401 + WinRTClientArgs, # type: ignore[unused-import] + ) + + assert recorder.list[0].filename == __file__ diff --git a/tests/bleak/corebluetooth/__init__.py b/tests/bleak/corebluetooth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/bleak/corebluetooth/test_deprecated_imports.py b/tests/bleak/corebluetooth/test_deprecated_imports.py new file mode 100644 index 000000000..7180b3d2d --- /dev/null +++ b/tests/bleak/corebluetooth/test_deprecated_imports.py @@ -0,0 +1,20 @@ +import sys + +import pytest + +# isort: off + +if not sys.platform.startswith("darwin"): + pytest.skip("backend only available on macOS", allow_module_level=True) + + +def test_deprecated_CBScannerArgs_import(): + with pytest.warns( + DeprecationWarning, + match="importing CBScannerArgs from bleak.backends.corebluetooth.scanner is deprecated, use bleak.args.corebluetooth instead", + ) as recorder: + from bleak.backends.corebluetooth.scanner import ( # noqa: F401 + CBScannerArgs, # type: ignore[unused-import] + ) + + assert recorder.list[0].filename == __file__ diff --git a/typings/CoreBluetooth/__init__.pyi b/typings/CoreBluetooth/__init__.pyi index ae988feb6..b484683d2 100644 --- a/typings/CoreBluetooth/__init__.pyi +++ b/typings/CoreBluetooth/__init__.pyi @@ -1,16 +1,13 @@ -from typing import Any, NewType, Optional, Type, TypeVar - -from ..Foundation import ( - NSUUID, - NSArray, - NSData, - NSDictionary, - NSError, - NSNumber, - NSObject, - NSString, -) -from ..libdispatch import dispatch_queue_t +import sys +from typing import Any, NewType, Optional, Protocol, TypeVar + +if sys.version_info < (3, 11): + from typing_extensions import Self +else: + from typing import Self + +from Foundation import NSUUID, NSArray, NSData, NSDictionary, NSError, NSObject +from libdispatch import dispatch_queue_t class CBManager(NSObject): def state(self) -> CBManagerState: ... @@ -18,31 +15,35 @@ class CBManager(NSObject): TCBCentralManager = TypeVar("TCBCentralManager", bound=CBCentralManager) class CBCentralManager(CBManager): - @classmethod - def init(cls: Type[TCBCentralManager]) -> Optional[TCBCentralManager]: ... @classmethod def initWithDelegate_queue_( - cls: Type[TCBCentralManager], + cls, delegate: CBCentralManagerDelegate, queue: dispatch_queue_t, - ) -> Optional[TCBCentralManager]: ... + ) -> Self: ... @classmethod def initWithDelegate_queue_options_( - cls: Type[TCBCentralManager], + cls, delegate: CBCentralManagerDelegate, queue: dispatch_queue_t, options: NSDictionary, - ) -> Optional[TCBCentralManager]: ... + ) -> Optional[Self]: ... def connectPeripheral_options_( - self, peripheral: CBPeripheral, options: Optional[NSDictionary] + self, + peripheral: CBPeripheral, + options: Optional[NSDictionary], ) -> None: ... def cancelPeripheralConnection_(self, peripheral: CBPeripheral) -> None: ... def retrieveConnectedPeripheralsWithServices_( - self, serviceUUIDs: NSArray - ) -> NSArray: ... - def retrievePeripheralsWithIdentifiers_(self, serviceUUIDs: NSArray) -> NSArray: ... + self, serviceUUIDs: NSArray[CBUUID] + ) -> NSArray[CBPeripheral]: ... + def retrievePeripheralsWithIdentifiers_( + self, serviceUUIDs: NSArray[CBUUID] + ) -> NSArray[CBPeripheral]: ... def scanForPeripheralsWithServices_options_( - self, serviceUUIDs: Optional[NSArray], options: Optional[NSDictionary] + self, + serviceUUIDs: Optional[NSArray[CBUUID]], + options: Optional[NSDictionary], ) -> None: ... def stopScan(self) -> None: ... def isScanning(self) -> bool: ... @@ -53,15 +54,15 @@ class CBCentralManager(CBManager): self, options: NSDictionary ) -> None: ... -CBConnectPeripheralOptionNotifyOnConnectionKey: NSString -CBConnectPeripheralOptionNotifyOnDisconnectionKey: NSString -CBConnectPeripheralOptionNotifyOnNotificationKey: NSString -CBConnectPeripheralOptionEnableTransportBridgingKey: NSString -CBConnectPeripheralOptionRequiresANCS: NSString -CBConnectPeripheralOptionStartDelayKey: NSString +CBConnectPeripheralOptionNotifyOnConnectionKey: str +CBConnectPeripheralOptionNotifyOnDisconnectionKey: str +CBConnectPeripheralOptionNotifyOnNotificationKey: str +CBConnectPeripheralOptionEnableTransportBridgingKey: str +CBConnectPeripheralOptionRequiresANCS: str +CBConnectPeripheralOptionStartDelayKey: str -CBCentralManagerScanOptionAllowDuplicatesKey: NSString -CBCentralManagerScanOptionSolicitedServiceUUIDsKey: NSString +CBCentralManagerScanOptionAllowDuplicatesKey: str +CBCentralManagerScanOptionSolicitedServiceUUIDsKey: str CBCentralManagerFeature = NewType("CBCentralManagerFeature", int) @@ -81,7 +82,7 @@ CBConnectionEvent = NewType("CBConnectionEvent", int) CBConnectionEventPeerConnected: CBConnectionEvent CBConnectionEventPeerDisconnected: CBConnectionEvent -class CBConnectionEventMatchingOption(NSString): ... +class CBConnectionEventMatchingOption(str): ... CBConnectionEventMatchingOptionPeripheralUUIDs: CBConnectionEventMatchingOption CBConnectionEventMatchingOptionServiceUUIDs: CBConnectionEventMatchingOption @@ -92,15 +93,16 @@ class CBPeer(NSObject): def identifier(self) -> NSUUID: ... class CBPeripheral(CBPeer): - def name(self) -> NSString: ... + def name(self) -> str: ... def delegate(self) -> CBPeripheralDelegate: ... - def discoverServices_(self, serviceUUIDs: NSArray) -> None: ... + def setDelegate_(self, delegate: CBPeripheralDelegate) -> None: ... + def discoverServices_(self, serviceUUIDs: Optional[NSArray[CBUUID]]) -> None: ... def discoverIncludedServices_forService_( - self, includedServiceUUIDs: NSArray, service: CBService + self, includedServiceUUIDs: NSArray[CBService], service: CBService ) -> None: ... - def services(self) -> NSArray: ... + def services(self) -> NSArray[CBService]: ... def discoverCharacteristics_forService_( - self, characteristicUUIDs: NSArray, service: CBService + self, characteristicUUIDs: Optional[NSArray[CBUUID]], service: CBService ) -> None: ... def discoverDescriptorsForCharacteristic_( self, characteristic: CBCharacteristic @@ -118,14 +120,14 @@ class CBPeripheral(CBPeer): ) -> None: ... def maximumWriteValueLengthForType_( self, type: CBCharacteristicWriteType - ) -> None: ... + ) -> int: ... def setNotifyValue_forCharacteristic_( self, enabled: bool, characteristic: CBCharacteristic ) -> None: ... def state(self) -> CBPeripheralState: ... def canSendWriteWithoutResponse(self) -> bool: ... def readRSSI(self) -> None: ... - def RSSI(self) -> NSNumber: ... + def RSSI(self) -> int: ... CBCharacteristicWriteType = NewType("CBCharacteristicWriteType", int) @@ -139,7 +141,7 @@ CBPeripheralStateConnecting: CBPeripheralState CBPeripheralStateConnected: CBPeripheralState CBPeripheralStateDisconnecting: CBPeripheralState -class CBPeripheralDelegate: +class CBPeripheralDelegate(Protocol): def peripheral_didDiscoverServices_( self, peripheral: CBPeripheral, error: Optional[NSError] ) -> None: ... @@ -191,12 +193,12 @@ class CBPeripheralDelegate: def peripheral_didReadRSSI_error_( self, peripheral: CBPeripheral, - RSSI: NSNumber, + RSSI: int, error: Optional[NSError], ) -> None: ... def peripheralDidUpdateName_(self, peripheral: CBPeripheral) -> None: ... def peripheral_didModifyServices_( - self, peripheral: CBPeripheral, invalidatedServices: NSArray + self, peripheral: CBPeripheral, invalidatedServices: NSArray[CBService] ) -> None: ... class CBAttribute(NSObject): @@ -205,25 +207,29 @@ class CBAttribute(NSObject): class CBService(CBAttribute): def peripheral(self) -> CBPeripheral: ... def isPrimary(self) -> bool: ... - def characteristics(self) -> Optional[NSArray]: ... - def includedServices(self) -> Optional[NSArray]: ... + def characteristics(self) -> NSArray[CBCharacteristic]: ... + def includedServices(self) -> Optional[NSArray[CBService]]: ... + # Undocumented property + def startHandle(self) -> int: ... class CBUUID(NSObject): @classmethod - def UUIDWithString_(cls, theString: NSString) -> CBUUID: ... + def UUIDWithString_(cls, theString: str) -> CBUUID: ... @classmethod def UUIDWithData_(cls, theData: NSData) -> CBUUID: ... @classmethod def UUIDWithNSUUID_(cls, theUUID: NSUUID) -> CBUUID: ... def data(self) -> NSData: ... - def UUIDString(self) -> NSString: ... + def UUIDString(self) -> str: ... class CBCharacteristic(CBAttribute): def service(self) -> CBService: ... def value(self) -> Optional[NSData]: ... - def descriptors(self) -> Optional[NSArray]: ... + def descriptors(self) -> NSArray[CBDescriptor]: ... def properties(self) -> CBCharacteristicProperties: ... def isNotifying(self) -> bool: ... + # Undocumented property + def handle(self) -> int: ... CBCharacteristicProperties = NewType("CBCharacteristicProperties", int) @@ -241,3 +247,5 @@ CBCharacteristicPropertyIndicateEncryptionRequired: CBCharacteristicProperties class CBDescriptor(CBAttribute): def characteristic(self) -> CBCharacteristic: ... def value(self) -> Optional[Any]: ... + # Undocumented property + def handle(self) -> int: ... diff --git a/typings/Foundation/__init__.pyi b/typings/Foundation/__init__.pyi index 21e4be06f..669dda24a 100644 --- a/typings/Foundation/__init__.pyi +++ b/typings/Foundation/__init__.pyi @@ -1,33 +1,66 @@ -from typing import NewType, Optional, Sequence, Type, TypeVar +import sys +from collections.abc import Iterator, Mapping, Sequence +from typing import Any, NewType, Optional, TypeVar, overload + +if sys.version_info < (3, 12): + from typing_extensions import Buffer +else: + from collections.abc import Buffer + +if sys.version_info < (3, 11): + from typing_extensions import Self +else: + from typing import Self TNSObject = TypeVar("TNSObject", bound=NSObject) class NSObject: @classmethod - def alloc(cls: Type[TNSObject]) -> TNSObject: ... - def init(self: TNSObject) -> Optional[TNSObject]: ... + def alloc(cls) -> Self: ... + def init(self) -> Optional[Self]: ... def addObserver_forKeyPath_options_context_( self, observer: NSObject, - keyPath: NSString, + keyPath: str, options: NSKeyValueObservingOptions, context: int, ) -> None: ... - def removeObserver_forKeyPath_( - self, observer: NSObject, keyPath: NSString - ) -> None: ... + def removeObserver_forKeyPath_(self, observer: NSObject, keyPath: str) -> None: ... + +class NSDictionary(NSObject, Mapping[str, Any]): + def __getitem__(self, key: str) -> Any: ... + def __iter__(self) -> Iterator[str]: ... + def __len__(self) -> int: ... + +class NSUUID(NSObject): + @classmethod + def UUIDWithString_(cls, uuidString: str) -> NSUUID: ... + def UUIDString(self) -> str: ... + def isEqualToUUID_(self, other: NSUUID) -> bool: ... -class NSDictionary(NSObject): ... -class NSUUID(NSObject): ... -class NSString(NSObject): ... class NSError(NSObject): ... -class NSData(NSObject): ... -class NSArray(NSObject): - def initWithArray_(self, array: Sequence) -> NSArray: ... +class NSData(NSObject, Buffer): + def initWithBytes_length_(self, bytes: Buffer, length: int) -> Self: ... + def length(self) -> int: ... + def getBytes_length_(self, buffer: bytes, length: int) -> None: ... + +T = TypeVar("T") + +class NSArray(NSObject, Sequence[T]): + @overload + def __getitem__(self, index: int) -> T: ... + @overload + def __getitem__(self, index: slice) -> Sequence[T]: ... + def __len__(self) -> int: ... + def initWithArray_(self, array: Sequence[Any]) -> Self: ... class NSValue(NSObject): ... -class NSNumber(NSValue): ... + +class NSBundle(NSObject): + @classmethod + def mainBundle(cls) -> NSBundle: ... + def bundleIdentifier(self) -> str: ... NSKeyValueObservingOptions = NewType("NSKeyValueObservingOptions", int) NSKeyValueObservingOptionNew: NSKeyValueObservingOptions @@ -35,7 +68,7 @@ NSKeyValueObservingOptionOld: NSKeyValueObservingOptions NSKeyValueObservingOptionInitial: NSKeyValueObservingOptions NSKeyValueObservingOptionPrior: NSKeyValueObservingOptions -NSKeyValueChangeKey = NewType("NSKeyValueChangeKey", NSString) +NSKeyValueChangeKey = NewType("NSKeyValueChangeKey", str) NSKeyValueChangeIndexesKey: NSKeyValueChangeKey NSKeyValueChangeKindKey: NSKeyValueChangeKey NSKeyValueChangeNewKey: NSKeyValueChangeKey diff --git a/typings/objc/__init__.py b/typings/objc/__init__.py deleted file mode 100644 index 0e92f6794..000000000 --- a/typings/objc/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Optional, Type, TypeVar - -from Foundation import NSObject - -T = TypeVar("T") - - -def super(cls: Type[T], self: T) -> T: ... - - -def macos_available(major: int, minor: int, patch: int = 0) -> bool: ... - - -class WeakRef: - def __init__(self, object: NSObject) -> None: ... - - def __call__(self) -> Optional[NSObject]: ... diff --git a/typings/objc/__init__.pyi b/typings/objc/__init__.pyi new file mode 100644 index 000000000..54e8e9a4b --- /dev/null +++ b/typings/objc/__init__.pyi @@ -0,0 +1,23 @@ +from typing import Literal, Optional, TypeVar, overload + +from CoreBluetooth import CBCentralManagerDelegate, CBPeripheralDelegate +from Foundation import NSObject + +T = TypeVar("T") + +def super(cls: type[T], self: T) -> T: ... +def macos_available(major: int, minor: int, patch: int = 0) -> bool: ... +def python_method(func: T) -> T: ... + +class WeakRef: + def __init__(self, object: NSObject) -> None: ... + def __call__(self) -> Optional[NSObject]: ... + +@overload +def protocolNamed( + name: Literal["CBCentralManagerDelegate"], +) -> type[CBCentralManagerDelegate]: ... +@overload +def protocolNamed( + name: Literal["CBPeripheralDelegate"], +) -> type[CBPeripheralDelegate]: ...