From e01e2640994b99066552b6161f84799712c396fa Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 3 Nov 2024 17:52:02 -0600 Subject: [PATCH 001/113] github: drop -dev from Python 3.13 --- .github/workflows/build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 22a5b50a1..6a4111772 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -14,7 +14,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.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} From a8996c5830815f12b220a5b09058a17fcc648ec2 Mon Sep 17 00:00:00 2001 From: Frederik Reiter Date: Tue, 14 Jan 2025 10:34:01 +0100 Subject: [PATCH 002/113] Log to stderr instead of stdout by default --- CHANGELOG.rst | 4 ++++ bleak/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c360d485..342da8d4e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,10 @@ and this project adheres to `Semantic Versioning Date: Tue, 4 Feb 2025 15:45:19 +0100 Subject: [PATCH 003/113] Update poetry to v2 --- CHANGELOG.rst | 4 +++ docs/conf.py | 2 +- poetry.lock | 86 ++++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 24 ++++++++------ 4 files changed, 102 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c360d485..af080b215 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,10 @@ and this project adheres to `Semantic Versioning =2.0``. + `0.22.3`_ (2024-10-05) ====================== diff --git a/docs/conf.py b/docs/conf.py index 9da6b5463..ec401fd21 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -67,7 +67,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 = tomli.load(f)["project"]["version"] # The short X.Y version. version = ".".join(release.split(".")[:2]) diff --git a/poetry.lock b/poetry.lock index dc89670d5..faa32817c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# 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.0.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -6,6 +6,7 @@ version = "0.7.12" description = "A configurable sidebar-enabled Sphinx theme" optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, @@ -17,6 +18,8 @@ version = "4.0.2" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.6" +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"}, @@ -28,6 +31,7 @@ version = "2.10.3" description = "Internationalization utilities" optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, @@ -42,6 +46,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"}, @@ -88,6 +93,8 @@ version = "1.2.0" description = "Python WinRT bindings for Bleak" optional = false python-versions = "*" +groups = ["main"] +markers = "platform_system == \"Windows\" and python_version < \"3.12\"" 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"}, @@ -108,6 +115,7 @@ version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["docs"] 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 +127,7 @@ 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"] 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 +142,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"}, @@ -147,10 +157,12 @@ version = "0.4.5" description = "Cross-platform colored terminal text." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +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"}, ] +markers = {docs = "sys_platform == \"win32\"", lint = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} [[package]] name = "coverage" @@ -158,6 +170,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"}, @@ -223,6 +236,8 @@ version = "2.0.0" description = "A faster version of dbus-next" optional = false python-versions = ">=3.7,<4.0" +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"}, @@ -266,6 +281,7 @@ version = "0.17.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["docs"] files = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, @@ -277,6 +293,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"}, @@ -291,6 +309,7 @@ version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.6.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"}, @@ -307,6 +326,7 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["docs"] files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, @@ -318,6 +338,7 @@ 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"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -329,6 +350,8 @@ version = "4.12.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.7" +groups = ["docs"] +markers = "python_version < \"3.10\"" 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"}, @@ -348,6 +371,7 @@ 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 +383,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"}, @@ -373,6 +398,7 @@ version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, @@ -390,6 +416,7 @@ version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["docs"] 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 +466,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 +478,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,6 +490,7 @@ 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"}, @@ -472,6 +502,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 +514,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 +530,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"}, @@ -513,6 +546,7 @@ version = "2.9.1" description = "Python style guide checker" optional = false python-versions = ">=3.6" +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"}, @@ -524,6 +558,7 @@ version = "2.5.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.6" +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"}, @@ -535,6 +570,7 @@ version = "2.15.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "Pygments-2.15.0-py3-none-any.whl", hash = "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094"}, {file = "Pygments-2.15.0.tar.gz", hash = "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"}, @@ -549,6 +585,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 +604,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 +626,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 +646,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 +669,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 +692,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 +711,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"}, @@ -683,6 +730,7 @@ version = "2022.2.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, @@ -694,6 +742,7 @@ version = "2.32.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, @@ -715,6 +764,7 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -726,6 +776,7 @@ version = "5.1.1" description = "Python documentation generator" optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "Sphinx-5.1.1-py3-none-any.whl", hash = "sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693"}, {file = "Sphinx-5.1.1.tar.gz", hash = "sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89"}, @@ -761,6 +812,7 @@ version = "1.0.0" description = "Read the Docs theme for Sphinx" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +groups = ["docs"] 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"}, @@ -779,6 +831,7 @@ version = "1.0.2" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.5" +groups = ["docs"] 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"}, @@ -794,6 +847,7 @@ version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." optional = false python-versions = ">=3.5" +groups = ["docs"] 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"}, @@ -809,6 +863,7 @@ version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.6" +groups = ["docs"] 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"}, @@ -824,6 +879,7 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["docs"] 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"}, @@ -838,6 +894,7 @@ version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." optional = false python-versions = ">=3.5" +groups = ["docs"] 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"}, @@ -853,6 +910,7 @@ version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." optional = false python-versions = ">=3.5" +groups = ["docs"] 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"}, @@ -868,6 +926,7 @@ version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" +groups = ["docs", "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"}, @@ -879,6 +938,8 @@ version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" optional = false python-versions = ">=3.7" +groups = ["main", "lint"] +markers = "python_version < \"3.12\"" 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"}, @@ -890,6 +951,7 @@ version = "1.26.19" 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" +groups = ["docs"] files = [ {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, @@ -906,6 +968,8 @@ version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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"}, @@ -931,6 +995,8 @@ version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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"}, @@ -962,6 +1028,8 @@ version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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"}, @@ -993,6 +1061,8 @@ version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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"}, @@ -1024,6 +1094,8 @@ version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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"}, @@ -1055,6 +1127,8 @@ version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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"}, @@ -1086,6 +1160,8 @@ version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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"}, @@ -1117,6 +1193,8 @@ version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" +groups = ["main"] +markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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"}, @@ -1148,6 +1226,8 @@ version = "3.19.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" +groups = ["docs"] +markers = "python_version < \"3.10\"" files = [ {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, @@ -1158,6 +1238,6 @@ doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linke 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)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.8,<3.14" -content-hash = "22885f16b59e72fb2bcf6fd88c81cabc7d48ebb05555f12375226200ca26c61e" +content-hash = "7b10e51f9218b1405f3beb3709712fb955e54ebff5f16cf405ab4faa6d197067" diff --git a/pyproject.toml b/pyproject.toml index 19559eefd..9cd11bacc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,21 @@ -[tool.poetry] +[project] name = "bleak" version = "0.22.3" 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.8" +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,11 +25,6 @@ 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" } @@ -55,7 +59,7 @@ 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] From 1f08aff703cec6c18bb4e744a472518959f0c940 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:00:43 +0100 Subject: [PATCH 004/113] Update black formatting --- bleak/backends/corebluetooth/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index 3491577df..866b5c442 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -63,7 +63,7 @@ def __init__( scanning_mode: Literal["active", "passive"], *, cb: CBScannerArgs, - **kwargs + **kwargs, ): super(BleakScannerCoreBluetooth, self).__init__( detection_callback, service_uuids From 028fe7121433ee561f6a6a1e88a26b2943481eec Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:58:30 +0100 Subject: [PATCH 005/113] Update flake8 to 7.1.1 to fix false-positives --- poetry.lock | 32 ++++++++++++++++---------------- pyproject.toml | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index faa32817c..2f4dffa78 100644 --- a/poetry.lock +++ b/poetry.lock @@ -305,20 +305,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" @@ -542,26 +542,26 @@ 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]] @@ -1239,5 +1239,5 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.1" -python-versions = ">=3.8,<3.14" -content-hash = "7b10e51f9218b1405f3beb3709712fb955e54ebff5f16cf405ab4faa6d197067" +python-versions = ">=3.8.1,<3.14" +content-hash = "df5ce1a0eb642e7888c72088de8f126757cb0bf90d9fd1308e9ce49a8a8c48ae" diff --git a/pyproject.toml b/pyproject.toml index 9cd11bacc..2ac931d2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = ">=3.8,<3.14" +python = ">=3.8.1,<3.14" async-timeout = { version = ">= 3.0.0, < 5", python = "<3.11" } typing-extensions = { version = ">=4.7.0", python = "<3.12" } pyobjc-core = { version = "^10.3", markers = "platform_system=='Darwin'" } @@ -50,7 +50,7 @@ tomli = "^2.0.1" [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] From afe58f1ca186fc7ffc2faa28d7e1d2281ad97b09 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 30 Mar 2025 10:00:13 -0500 Subject: [PATCH 006/113] setup.cfg: ignore Flake E231 Ignore Flake E231. It disagrees with Black formatting. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b268261fc890d8bcee3aae422d84bea16b07c336 Mon Sep 17 00:00:00 2001 From: Patrick <14628713+patman15@users.noreply.github.com> Date: Sun, 6 Apr 2025 01:21:32 +0200 Subject: [PATCH 007/113] bleak: deprecate omitting write_gatt_char() response arg Also fix the type hints while we are touching it (#1730). Co-authored-by: David Lechner --- CHANGELOG.rst | 5 +++++ bleak/__init__.py | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c360d485..7b94dd4d4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,11 @@ and this project adheres to `Semantic Versioning None: r""" Perform a write operation on the specified GATT characteristic. @@ -738,10 +738,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,12 +754,15 @@ 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. .. versionchanged:: 0.21 The default behavior when ``response=`` is omitted was changed. + .. versionchanged:: unreleased + Omitting the ``response=`` argument is deprecated and may be required + in a future release. + Example:: MY_CHAR_UUID = "1234" @@ -781,6 +780,12 @@ async def write_gatt_char( if response is None: # if not specified, prefer write-with-response over write-without- # response if it is available since it is the more reliable write. + warn( + "Omitting the response argument is not recommended." + "Legacy usage may be not allowed in the future.", + DeprecationWarning, + stacklevel=2, + ) response = "write" in characteristic.properties await self._backend.write_gatt_char(characteristic, data, response) From 39dd21e351093fe852c116d33b5037cded752bf8 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 5 Apr 2025 18:39:55 -0500 Subject: [PATCH 008/113] CHANGELOG.rst: fix formatting --- CHANGELOG.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5828ba9e8..1a65fdc14 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,9 +11,9 @@ and this project adheres to `Semantic Versioning =2.0``. `0.22.3`_ (2024-10-05) From ff044d1448f1f7befaf6008249b5ca16d0611d5d Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 5 Apr 2025 19:47:51 -0500 Subject: [PATCH 009/113] pyproject: drop support for Python 3.8 --- .github/pull_request_template.md | 2 +- .github/workflows/build_and_test.yml | 2 +- CHANGELOG.rst | 4 ++ CONTRIBUTING.rst | 2 +- poetry.lock | 103 +++++++++++++++------------ pyproject.toml | 19 +++-- 6 files changed, 73 insertions(+), 59 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 33e3fb6e6..a06f44385 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ 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 diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 6a4111772..fb9ef8aa7 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -14,7 +14,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'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d3d00c268..98ba36a2c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,10 @@ Changed * Updatd Poetry build system version to ``>=2.0``. * Log to stderr instead of stdout when ``BLEAK_LOGGING`` is enabled. +Removed +------- +* Removed support for Python 3.8. The minimum supported version is now Python 3.9. + `0.22.3`_ (2024-10-05) ====================== diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9f9f26925..30847c766 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -102,7 +102,7 @@ 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 diff --git a/poetry.lock b/poetry.lock index 2f4dffa78..a252342c5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -232,47 +232,58 @@ toml = ["tomli"] [[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]] @@ -969,7 +980,7 @@ description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] -markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" +markers = "python_version >= \"3.12\" and python_version < \"3.14\" and 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"}, @@ -996,7 +1007,7 @@ description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] -markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" +markers = "python_version >= \"3.12\" and python_version < \"3.14\" and 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"}, @@ -1029,7 +1040,7 @@ description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] -markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" +markers = "python_version >= \"3.12\" and python_version < \"3.14\" and 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"}, @@ -1062,7 +1073,7 @@ description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] -markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" +markers = "python_version >= \"3.12\" and python_version < \"3.14\" and 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"}, @@ -1095,7 +1106,7 @@ description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] -markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" +markers = "python_version >= \"3.12\" and python_version < \"3.14\" and 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"}, @@ -1128,7 +1139,7 @@ description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] -markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" +markers = "python_version >= \"3.12\" and python_version < \"3.14\" and 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"}, @@ -1161,7 +1172,7 @@ description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] -markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" +markers = "python_version >= \"3.12\" and python_version < \"3.14\" and 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"}, @@ -1194,7 +1205,7 @@ description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] -markers = "platform_system == \"Windows\" and python_version >= \"3.12\"" +markers = "python_version >= \"3.12\" and python_version < \"3.14\" and 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"}, @@ -1239,5 +1250,5 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.1" -python-versions = ">=3.8.1,<3.14" -content-hash = "df5ce1a0eb642e7888c72088de8f126757cb0bf90d9fd1308e9ce49a8a8c48ae" +python-versions = ">=3.9" +content-hash = "2b2afea463fbe43d8a500356c80886fe22e1c49247e1b4c4078b67f0bbc869b9" diff --git a/pyproject.toml b/pyproject.toml index 2ac931d2e..cdeda0cc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Bluetooth Low Energy platform Agnostic Klient" authors = [{ name = "Henrik Blidh", email = "henrik.blidh@nedomkull.com" }] license = "MIT" readme = "README.rst" -requires-python = ">=3.8" +requires-python = ">=3.9" dynamic = ["classifiers", "dependencies"] [project.urls] @@ -26,21 +26,20 @@ classifiers = [ ] [tool.poetry.dependencies] -python = ">=3.8.1,<3.14" async-timeout = { version = ">= 3.0.0, < 5", 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" } +"winrt-runtime" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } +"winrt-Windows.Devices.Bluetooth" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } +"winrt-Windows.Devices.Bluetooth.Advertisement" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } +"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } +"winrt-Windows.Devices.Enumeration" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } +"winrt-Windows.Foundation" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } +"winrt-Windows.Foundation.Collections" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } +"winrt-Windows.Storage.Streams" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } dbus-fast = { version = ">=1.83.0, < 3", markers = "platform_system == 'Linux'" } [tool.poetry.group.docs.dependencies] From 0f913d06716054b8ceb3dc750e893a9a4373f113 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 09:11:00 -0500 Subject: [PATCH 010/113] CHANGELOG: fix typo --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 98ba36a2c..888bc5e0e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,7 @@ Changed ------- * Made `response` argument of class ``BleakClient.write_gatt_char()`` explicitly optional. #1730 * Deprecated omitting the ``response`` argument. -* Updatd Poetry build system version to ``>=2.0``. +* Updated Poetry build system version to ``>=2.0``. * Log to stderr instead of stdout when ``BLEAK_LOGGING`` is enabled. Removed From 7feb2fba8e55feff41b35c83cd32950d0e1c42ad Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 09:14:26 -0500 Subject: [PATCH 011/113] pyproject: remove upper bound on most dependency versions Unless there is a know breaking change that actually affects us, having upper bounds on dependencies in libraries can actually be problematic for people using the library who may need newer versions to satisfy other dependencies. --- poetry.lock | 2 +- pyproject.toml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index a252342c5..b79a056e6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1251,4 +1251,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "2b2afea463fbe43d8a500356c80886fe22e1c49247e1b4c4078b67f0bbc869b9" +content-hash = "7860b15944f4833be3db2c30e20045e11f766cfed6c9219d2e9766387b6c2df2" diff --git a/pyproject.toml b/pyproject.toml index cdeda0cc5..d102cadf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,11 +26,11 @@ classifiers = [ ] [tool.poetry.dependencies] -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'" } +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,<3.14" } "winrt-Windows.Devices.Bluetooth" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } @@ -40,7 +40,7 @@ bleak-winrt = { version = "^1.2.0", markers = "platform_system=='Windows'", pyth "winrt-Windows.Foundation" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } "winrt-Windows.Foundation.Collections" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } "winrt-Windows.Storage.Streams" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } -dbus-fast = { version = ">=1.83.0, < 3", markers = "platform_system == 'Linux'" } +dbus-fast = { version = ">=1.83.0", markers = "platform_system == 'Linux'" } [tool.poetry.group.docs.dependencies] Sphinx = "^5.1.1" From 808cdf3934a82759d16c1025f2a85b100b6cb110 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 10:36:51 -0500 Subject: [PATCH 012/113] winrt: Update to v3.1 There are known bugs in older versions of the winrt libraries. Drop bleak_winrt and use winrt >= 3.1 on all Python versions now. There were some major breaking API name changes, so there is quite a bit of churn in the code as well as some improvements to type hints. --- CHANGELOG.rst | 1 + bleak/backends/winrt/characteristic.py | 15 +- bleak/backends/winrt/client.py | 270 ++++++++++-------- bleak/backends/winrt/descriptor.py | 10 +- bleak/backends/winrt/scanner.py | 49 ++-- bleak/backends/winrt/service.py | 10 +- docs/conf.py | 1 - poetry.lock | 371 ++++++++++++------------- pyproject.toml | 17 +- 9 files changed, 363 insertions(+), 381 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 888bc5e0e..5d0dbe93f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,7 @@ Changed * Deprecated omitting the ``response`` argument. * Updated Poetry build system version to ``>=2.0``. * Log to stderr instead of stdout when ``BLEAK_LOGGING`` is enabled. +* Updated ``winrt`` backend to use PyWinRT >= 3.1. Removed ------- diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py index 6a576bf6b..0bad1c6ad 100644 --- a/bleak/backends/winrt/characteristic.py +++ b/bleak/backends/winrt/characteristic.py @@ -1,18 +1,11 @@ # -*- 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 winrt.windows.devices.bluetooth.genericattributeprofile import ( + GattCharacteristic, + GattCharacteristicProperties, +) from ..characteristic import BleakGATTCharacteristic from ..descriptor import BleakGATTDescriptor diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index a04ec7c59..c274d463b 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -14,6 +14,7 @@ from typing import ( Any, Dict, + Generic, List, Literal, Optional, @@ -21,6 +22,7 @@ Sequence, Set, TypedDict, + TypeVar, Union, cast, ) @@ -35,70 +37,40 @@ 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 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 winrt.windows.storage.streams import Buffer as WinBuffer from ... import BleakScanner from ...exc import ( @@ -120,8 +92,10 @@ 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 +130,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}" @@ -229,8 +204,8 @@ def __init__( 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] = {} + self._session: Optional[GattSession] = None + self._notification_callbacks: Dict[int, EventRegistrationToken] = {} if "address_type" in kwargs: warnings.warn( @@ -513,10 +488,15 @@ async def disconnect(self) -> bool: """ logger.debug("Disconnecting from BLE device...") + + assert self.services + # 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. @@ -572,7 +552,9 @@ 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: + async def pair( + self, protection_level: Optional[DevicePairingProtectionLevel] = None, **kwargs + ) -> bool: """Attempts to pair with the device. Keyword Args: @@ -587,6 +569,8 @@ async def pair(self, protection_level: int = None, **kwargs) -> bool: Boolean regarding success of pairing. """ + 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 @@ -599,14 +583,19 @@ async def pair(self, protection_level: int = None, **kwargs) -> bool: ceremony = DevicePairingKinds.CONFIRM_ONLY custom_pairing = device_information.pairing.custom - def handler(sender, args): + def handler( + sender: DeviceInformationCustomPairing, + args: DevicePairingRequestedEventArgs, + ): args.accept() pairing_requested_token = custom_pairing.add_pairing_requested(handler) try: - if protection_level: - pairing_result = await custom_pairing.pair_async( - ceremony, protection_level + if protection_level is not None: + pairing_result = ( + await custom_pairing.pair_with_protection_level_async( + ceremony, protection_level + ) ) else: pairing_result = await custom_pairing.pair_async(ceremony) @@ -687,37 +676,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 +700,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 +721,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 @@ -759,8 +745,19 @@ def dispose_on_cancel(future): new_services.add_service(BleakGATTServiceWinRT(service)) 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})", ) @@ -820,6 +817,8 @@ 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): @@ -829,9 +828,11 @@ async def read_gatt_char( 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 @@ -862,15 +863,19 @@ async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: if not self.is_connected: raise BleakError("Not connected") + assert self.services + use_cached = kwargs.get("use_cached", False) descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError(f"Descriptor with handle {handle} was not found!") + 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 @@ -893,17 +898,23 @@ async def write_gatt_char( 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}", ) @@ -919,16 +930,22 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: if not self.is_connected: raise BleakError("Not connected") + assert self.services + descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError(f"Descriptor with handle {handle} was not found!") 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}", ) @@ -978,7 +995,7 @@ def handle_value_changed( 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, @@ -1009,6 +1026,8 @@ async def stop_notify( if not self.is_connected: raise BleakError("Not connected") + assert self.services + if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: @@ -1016,8 +1035,10 @@ async def stop_notify( if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) + 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 +1046,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) -class FutureLike: +T = TypeVar("T") + + +class FutureLike(Generic[T]): """ Wraps a WinRT IAsyncOperation in a "future-like" object so that it can be passed to Python APIs. @@ -1038,7 +1062,7 @@ class FutureLike: _asyncio_future_blocking = False - def __init__(self, op: IAsyncOperation) -> None: + def __init__(self, op: IAsyncOperation[T]) -> None: self._op = op self._callbacks = [] self._loop = asyncio.get_running_loop() @@ -1058,7 +1082,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 +1090,8 @@ def result(self) -> Any: if self._cancel_requested: raise asyncio.CancelledError + assert self._result + return self._result if self._op.status == AsyncStatus.CANCELED: diff --git a/bleak/backends/winrt/descriptor.py b/bleak/backends/winrt/descriptor.py index 1203b754b..8eb493194 100644 --- a/bleak/backends/winrt/descriptor.py +++ b/bleak/backends/winrt/descriptor.py @@ -1,12 +1,6 @@ # -*- 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 winrt.windows.devices.bluetooth.genericattributeprofile import GattDescriptor from ..descriptor import BleakGATTDescriptor diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index 723ae1fe1..d4a14f4db 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -1,32 +1,22 @@ import asyncio import logging -import sys from typing import Dict, List, 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, - ) -else: - from bleak_winrt.windows.devices.bluetooth.advertisement import ( - BluetoothLEAdvertisementReceivedEventArgs, - BluetoothLEAdvertisementType, - BluetoothLEAdvertisementWatcher, - BluetoothLEAdvertisementWatcherStatus, - BluetoothLEScanningMode, - ) +from winrt.windows.devices.bluetooth.advertisement import ( + BluetoothLEAdvertisementReceivedEventArgs, + BluetoothLEAdvertisementType, + BluetoothLEAdvertisementWatcher, + BluetoothLEAdvertisementWatcherStatus, + BluetoothLEScanningMode, +) +from winrt.windows.foundation import EventRegistrationToken from ...assigned_numbers import AdvertisementDataType from ...exc import BleakError from ...uuids import normalize_uuid_str from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner +from .util import assert_mta logger = logging.getLogger(__name__) @@ -87,8 +77,8 @@ def __init__( 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 +94,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, @@ -144,6 +134,8 @@ def _received_handler( 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 +147,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,7 +190,7 @@ 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), ) @@ -259,6 +251,11 @@ async def start(self) -> None: raise BleakError(f"Unexpected watcher status: {self.watcher.status.name}") 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: diff --git a/bleak/backends/winrt/service.py b/bleak/backends/winrt/service.py index dde2d4ea2..e387f3fd3 100644 --- a/bleak/backends/winrt/service.py +++ b/bleak/backends/winrt/service.py @@ -1,14 +1,6 @@ -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 winrt.windows.devices.bluetooth.genericattributeprofile import GattDeviceService from ..service import BleakGATTService from ..winrt.characteristic import BleakGATTCharacteristicWinRT diff --git a/docs/conf.py b/docs/conf.py index ec401fd21..a7d618111 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -115,7 +115,6 @@ autodoc_mock_imports = [ "android", "async_timeout", - "bleak_winrt", "winrt", "CoreBluetooth", "dbus_fast", diff --git a/poetry.lock b/poetry.lock index b79a056e6..7c6fa0e76 100644 --- a/poetry.lock +++ b/poetry.lock @@ -87,28 +87,6 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 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 = "*" -groups = ["main"] -markers = "platform_system == \"Windows\" and python_version < \"3.12\"" -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" @@ -945,16 +923,16 @@ files = [ [[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"] -markers = "python_version < \"3.12\"" 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" @@ -975,261 +953,264 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.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 = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" +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 = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" +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 = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" +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 = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" +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 = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" +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 = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" +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 = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" +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 = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" +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" +winrt-runtime = "3.1.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)"] +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)"] [[package]] name = "zipp" @@ -1251,4 +1232,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "7860b15944f4833be3db2c30e20045e11f766cfed6c9219d2e9766387b6c2df2" +content-hash = "28096101bee9ebdfb7c475df12670b3b2e3c3224071f3370f0b9b431602be765" diff --git a/pyproject.toml b/pyproject.toml index d102cadf2..2e2c3f0f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,15 +31,14 @@ 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,<3.14" } -"winrt-Windows.Devices.Bluetooth" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } -"winrt-Windows.Devices.Bluetooth.Advertisement" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } -"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } -"winrt-Windows.Devices.Enumeration" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } -"winrt-Windows.Foundation" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } -"winrt-Windows.Foundation.Collections" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } -"winrt-Windows.Storage.Streams" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12,<3.14" } +"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.dependencies] From 89eae4761827fab2f8169ce7833647a5ac708797 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 11:04:42 -0500 Subject: [PATCH 013/113] bleak: restore description of response=None Fixes: https://github.com/hbldh/bleak/issues/1736 --- bleak/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bleak/__init__.py b/bleak/__init__.py index 45c170ab6..44d6e8fe9 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -755,6 +755,11 @@ async def write_gatt_char( If ``True``, a write-with-response operation will be used. If ``False``, a write-without-response operation will be used. 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. .. versionchanged:: 0.21 The default behavior when ``response=`` is omitted was changed. From c4b6905f3c6a01bf2eeba058c5c20e133d974c4f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 15:57:49 -0500 Subject: [PATCH 014/113] bluez: fix service interface leak Fix a leak where service interfaces were not being removed from the service map in the BlueZ manager. This could cause a KeyError on later connections when calling get_services(). Fixes: https://github.com/hbldh/bleak/issues/1435 --- CHANGELOG.rst | 4 ++++ bleak/backends/bluezdbus/manager.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5d0dbe93f..e3dbc3e47 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,10 @@ Changed * Log to stderr instead of stdout when ``BLEAK_LOGGING`` is enabled. * Updated ``winrt`` backend to use PyWinRT >= 3.1. +Fixed +----- +* Fixed possible ``KeyError`` when getting services in BlueZ backend. Fixes #1435. + Removed ------- * Removed support for Python 3.8. The minimum supported version is now Python 3.9. diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index cedbd6420..5c65170a4 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -945,6 +945,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: From a1ce05e251664b559f9325be0b8f0f26a8012ce7 Mon Sep 17 00:00:00 2001 From: Koen van Steenbergen Date: Mon, 16 Dec 2024 11:02:55 +0100 Subject: [PATCH 015/113] bluez: Fix DBus connection leak by utilizing AsyncExitStack The try/except blocks in the `BluezDBusClient` connect menthod were cleaning up a few things on failure, but not everything. So we need one more level of cleanup that calls _cleanup_all(). Since this is getting too indented, switch to using an AsyncExitStack to handle the cleanup. --- CHANGELOG.rst | 3 + bleak/backends/bluezdbus/client.py | 262 ++++++++++++++--------------- 2 files changed, 132 insertions(+), 133 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e3dbc3e47..05bc94e05 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,8 @@ and this project adheres to `Semantic Versioning 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 + # 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, @@ -294,7 +219,7 @@ def on_value_changed(char_path: str, value: bytes) -> None: ) ) 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 +231,84 @@ 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) + 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) + + 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 True @staticmethod async def _disconnect_monitor( From 534b58a208c04942b366858d33b66b891ef61a6a Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 23:15:07 -0500 Subject: [PATCH 016/113] docs: ignore .venv dircectory It is convenient to create a separate virtual environment for the docs. However, docs builds can fail because it would otherwise recurse into this directory. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index a7d618111..642c7df71 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -83,7 +83,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. From 2b718a509e8e9c6e0c85ff668b30485887f9afbd Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 22:19:00 -0500 Subject: [PATCH 017/113] winrt: improve type hints for adv data args --- bleak/backends/winrt/client.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index c274d463b..87350c3c8 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -85,7 +85,7 @@ from ..service import BleakGATTServiceCollection from .characteristic import BleakGATTCharacteristicWinRT from .descriptor import BleakGATTDescriptorWinRT -from .scanner import BleakScannerWinRT +from .scanner import BleakScannerWinRT, _RawAdvData from .service import BleakGATTServiceWinRT logger = logging.getLogger(__name__) @@ -193,10 +193,13 @@ def __init__( # 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 ) @@ -269,8 +272,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) From d4995d2259fc3ed33f52d31ff36c1bef0e99fb9f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 22:20:22 -0500 Subject: [PATCH 018/113] winrt: fix another winrt v3 api change This was missed in 808cdf3. --- bleak/backends/winrt/client.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 87350c3c8..958a53646 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -232,16 +232,19 @@ def __str__(self): # 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: From 309f20078b5b9ad7ccb2458968d6cbf3fffbf579 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 22:35:54 -0500 Subject: [PATCH 019/113] winrt: improve logging in pair method Also reformat to return early on already paired devices for less indentation. --- bleak/backends/winrt/client.py | 78 +++++++++++++++++----------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 958a53646..c035a27de 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -583,49 +583,51 @@ async def pair( 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 - ): - # Currently only supporting Just Works solutions... - ceremony = DevicePairingKinds.CONFIRM_ONLY - custom_pairing = device_information.pairing.custom - def handler( - sender: DeviceInformationCustomPairing, - args: DevicePairingRequestedEventArgs, - ): - args.accept() + if not device_information.pairing.can_pair: + logging.debug("Device does not support pairing. Skipping pairing.") + return False - pairing_requested_token = custom_pairing.add_pairing_requested(handler) - try: - if protection_level is not None: - pairing_result = ( - await custom_pairing.pair_with_protection_level_async( - ceremony, protection_level - ) - ) - else: - pairing_result = await custom_pairing.pair_async(ceremony) + if device_information.pairing.is_paired: + logging.debug("Device is already paired. Skipping pairing.") + return True - except Exception as e: - raise BleakError("Failure trying to pair with device!") from e - finally: - custom_pairing.remove_pairing_requested(pairing_requested_token) + # Currently only supporting Just Works solutions... + ceremony = DevicePairingKinds.CONFIRM_ONLY + custom_pairing = device_information.pairing.custom - 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, + def handler( + sender: DeviceInformationCustomPairing, + args: DevicePairingRequestedEventArgs, + ): + args.accept() + + pairing_requested_token = custom_pairing.add_pairing_requested(handler) + + try: + if protection_level is not None: + pairing_result = await custom_pairing.pair_with_protection_level_async( + ceremony, protection_level ) - return True - else: - return device_information.pairing.is_paired + 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) + + if pairing_result.status not in ( + DevicePairingResultStatus.PAIRED, + DevicePairingResultStatus.ALREADY_PAIRED, + ): + raise BleakError(f"Could not pair with device: {pairing_result.status}") + + logger.debug( + "Paired to device with protection level %r.", + pairing_result.protection_level_used, + ) + return True async def unpair(self) -> bool: """Attempts to unpair from the device. From fc8fa2ad3ea2d1e88321e184aa79bc686d2d862b Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 22:36:45 -0500 Subject: [PATCH 020/113] winrt: move event wait inside timeout Avoid a deadlock when event doesn't come by allowing it to timeout. --- CHANGELOG.rst | 1 + bleak/backends/winrt/client.py | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 05bc94e05..8afc6b0a6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Fixed ----- * 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. Removed ------- diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index c035a27de..2d8135d68 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -472,10 +472,11 @@ def max_pdu_size_changed_handler(sender: GattSession, args): 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) From 0c5b1e5d42f3b0084658834623658ab880a808f6 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Mon, 5 May 2025 23:22:04 +0200 Subject: [PATCH 021/113] make extraction of device path from characteristic path more robust choose all characters up to the last 21. This is needed, since the hci device can have an index >= 10 and hence the number of characters of the prefix is not fixed. Instead, the suffix length is fixed as it comes from https://github.com/bluez/bluez/blob/313de9af36cbbd02d69ba12b8819e28f6a89bbd5/src/gatt-client.c#L2011 https://github.com/bluez/bluez/blob/313de9af36cbbd02d69ba12b8819e28f6a89bbd5/src/gatt-client.c#L1835 --- CHANGELOG.rst | 1 + bleak/backends/bluezdbus/utils.py | 2 +- tests/bleak/backends/bluezdbus/test_utils.py | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8afc6b0a6..3669c7a87 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,7 @@ Fixed * 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. +* Fixed getting notifications from devices connected to BLE adapters with index >9 (hci10, hci11, ...) Removed ------- diff --git a/bleak/backends/bluezdbus/utils.py b/bleak/backends/bluezdbus/utils.py index e1acf99f4..abf81c4f6 100644 --- a/bleak/backends/bluezdbus/utils.py +++ b/bleak/backends/bluezdbus/utils.py @@ -52,7 +52,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/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" + ) From 52a53fe371cf3ef1a319be2ab903d2a119cbec04 Mon Sep 17 00:00:00 2001 From: Alan Fischer Date: Sat, 10 May 2025 10:32:17 -0600 Subject: [PATCH 022/113] Corebluetooth: add notification descriminator (#1742) Add a new callback that can help discriminate between characteristic value changes due to a notification and ones due to a read response. CoreBluetooth uses the same delegate callback for both and doesn't have any info available in the parameters to know what is the difference between the two. To work around this allow the user to provide a discriminator function that can inspect the value to determine if it was from a notification. This way, if a notification happens while we are waiting for a read response, we don't accidentally send the notification value as the read response value. --- CHANGELOG.rst | 1 + .../corebluetooth/PeripheralDelegate.py | 40 +++++++++++++--- bleak/backends/corebluetooth/client.py | 10 +++- docs/backends/macos.rst | 46 +++++++++++++++++-- 4 files changed, 85 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3669c7a87..7bfdc4748 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,7 @@ Changed * Updated Poetry build system version to ``>=2.0``. * Log to stderr instead of stdout when ``BLEAK_LOGGING`` is enabled. * Updated ``winrt`` backend to use PyWinRT >= 3.1. +* Added ``notification_discriminator`` parameter to start_notify on CoreBluetooth backend Fixed ----- diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index d48f477c8..9726a4295 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -12,7 +12,7 @@ import itertools import logging import sys -from typing import Any, Dict, Iterable, NewType, Optional +from typing import Any, Callable, Dict, Iterable, NewType, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout @@ -71,6 +71,9 @@ def initWithPeripheral_( self._characteristic_notify_change_futures: Dict[int, asyncio.Future] = {} self._characteristic_notify_callbacks: Dict[int, NotifyCallback] = {} + self._characteristic_notification_discriminators: Dict[ + int, Callable[[bytearray], bool] + ] = {} self._read_rssi_futures: Dict[NSUUID, asyncio.Future] = {} @@ -153,6 +156,7 @@ async def read_characteristic( future = self._event_loop.create_future() self._characteristic_read_futures[characteristic.handle()] = future + try: self.peripheral.readValueForCharacteristic_(characteristic) async with async_timeout(timeout): @@ -214,13 +218,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[Callable[[bytes], bool]] = 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,6 +257,7 @@ 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: @@ -371,14 +382,31 @@ def did_update_value_for_characteristic( 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: + 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: diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index a682dadbd..1cfa993ec 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -8,7 +8,7 @@ import logging import sys import uuid -from typing import Optional, Set, Union +from typing import Any, Dict, Optional, Set, Union if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -354,6 +354,8 @@ async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, + *, + cb: Dict[str, Any] = {}, **kwargs, ) -> None: """ @@ -361,7 +363,11 @@ async def start_notify( """ 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] diff --git a/docs/backends/macos.rst b/docs/backends/macos.rst index f4af7a016..67a0b1897 100644 --- a/docs/backends/macos.rst +++ b/docs/backends/macos.rst @@ -27,20 +27,58 @@ 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. + +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 has can accept a ``notification_discriminator`` callback in the ``cb`` dict parameter that is +passed to the :meth:`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=dict(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: From 00937c492589e4f2e7b256e455fc56726429a91a Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 19 Apr 2025 22:39:52 -0500 Subject: [PATCH 023/113] bleak: add pair param to BleakClient() Some device need to pair _before_ connecting, otherwise getting services doesn't work correctly. This adds a `pair` parameter to the BleakClient constructor that allows this to happen during the connection process. --- CHANGELOG.rst | 4 ++ bleak/__init__.py | 14 ++++++- bleak/backends/bluezdbus/client.py | 53 ++++++++++++++++++++++---- bleak/backends/bluezdbus/manager.py | 15 ++++++++ bleak/backends/client.py | 9 ++++- bleak/backends/corebluetooth/client.py | 5 ++- bleak/backends/p4android/client.py | 5 ++- bleak/backends/winrt/client.py | 12 +++++- docs/api/client.rst | 6 +++ examples/service_explorer.py | 20 +++++++++- 10 files changed, 128 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7bfdc4748..b65a6ffaa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,10 @@ and this project adheres to `Semantic Versioning bool: Always returns ``True`` for backwards compatibility. """ - return await self._backend.connect(**kwargs) + return await self._backend.connect(self._pair_before_connect, **kwargs) async def disconnect(self) -> bool: """Disconnect from the specified GATT server. diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 6ff859bb6..0181bc064 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -105,7 +105,9 @@ def __init__( # Connectivity methods - async def connect(self, dangerous_use_bleak_cache: bool = False, **kwargs) -> bool: + async def connect( + self, pair: bool, dangerous_use_bleak_cache: bool = False, **kwargs + ) -> bool: """Connect to the specified GATT server. Keyword Args: @@ -246,14 +248,49 @@ async def disconnect_device() -> None: ) 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", + + # 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(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", + ) + ) + else: + 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 diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index 5c65170a4..494676fea 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -742,6 +742,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. diff --git a/bleak/backends/client.py b/bleak/backends/client.py index ddf77f2f3..789a996f0 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -78,9 +78,16 @@ def set_disconnected_callback( self._disconnected_callback = callback @abc.abstractmethod - async def connect(self, **kwargs) -> bool: + async def connect(self, pair: bool, **kwargs) -> bool: """Connect to the specified GATT server. + 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. + Returns: Boolean representing connection status. diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 1cfa993ec..1bc033a71 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -84,7 +84,7 @@ def __init__( def __str__(self) -> str: return "BleakClientCoreBluetooth ({})".format(self.address) - async def connect(self, **kwargs) -> bool: + async def connect(self, pair: bool, **kwargs) -> bool: """Connect to a specified Peripheral Keyword Args: @@ -94,6 +94,9 @@ async def connect(self, **kwargs) -> bool: 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( diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index f1bca4dd5..360e3e534 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -57,13 +57,16 @@ def __del__(self): # Connectivity methods - async def connect(self, **kwargs) -> bool: + async def connect(self, pair: bool, **kwargs) -> bool: """Connect to the specified GATT server. Returns: Boolean representing connection status. """ + if pair: + logger.warning("Pairing during connect is not implemented on Android") + loop = asyncio.get_running_loop() self.__adapter = defs.BluetoothAdapter.getDefaultAdapter() diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 2d8135d68..a39176f56 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -253,7 +253,7 @@ async def _create_requester(self, bluetooth_address: int) -> BluetoothLEDevice: ) return requester - async def connect(self, **kwargs) -> bool: + async def connect(self, pair: bool, **kwargs) -> bool: """Connect to the specified GATT server. Keyword Args: @@ -286,6 +286,9 @@ async def connect(self, **kwargs) -> bool: self._requester = await self._create_requester(self._device_info) + if pair: + await self.pair(**kwargs) + def handle_services_changed(): if not self._services_changed_events: logger.warning("%s: unhandled services changed event", self.address) @@ -394,6 +397,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 ) diff --git a/docs/api/client.rst b/docs/api/client.rst index b255f1e76..bc7e079e4 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -2,6 +2,8 @@ BleakClient class ================= +.. currentmodule:: bleak + .. autoclass:: bleak.BleakClient ---------------------------- @@ -101,6 +103,10 @@ 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 diff --git a/examples/service_explorer.py b/examples/service_explorer.py index 5649bb545..dfa558b99 100644 --- a/examples/service_explorer.py +++ b/examples/service_explorer.py @@ -18,7 +18,16 @@ logger = logging.getLogger(__name__) -async def main(args: argparse.Namespace): +class Args(argparse.Namespace): + name: str + address: str + macos_use_bdaddr: bool + services: list[str] + pair: bool + debug: bool + + +async def main(args: Args): logger.info("starting scan...") if args.address: @@ -40,6 +49,7 @@ async def main(args: argparse.Namespace): async with BleakClient( device, + pair=args.pair, services=args.services, ) as client: logger.info("connected") @@ -108,6 +118,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 +131,7 @@ async def main(args: argparse.Namespace): help="sets the log level to debug", ) - args = parser.parse_args() + args: Args = parser.parse_args() log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( From 8dea852f35e49e5f8ebb93a47690ad4093741e4e Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 10 May 2025 13:31:54 -0500 Subject: [PATCH 024/113] bluez/characteristic: remove unused _GattCharacteristicsFlagsEnum --- bleak/backends/bluezdbus/characteristic.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/bleak/backends/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py index ab707827a..7808c26ef 100644 --- a/bleak/backends/bluezdbus/characteristic.py +++ b/bleak/backends/bluezdbus/characteristic.py @@ -6,26 +6,6 @@ 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""" From 8c5ad923bc1b6ab90364ac416df657f64c8765ef Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 10 May 2025 12:05:05 -0500 Subject: [PATCH 025/113] docs: remove requirements.txt This is handled in the pyproject.toml file now. --- docs/requirements.txt | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 docs/requirements.txt 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 From 25f7279e9d8d9504c84c0451b4c2548573dab0a5 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 10 May 2025 12:20:54 -0500 Subject: [PATCH 026/113] pyproject: update docs dependencies --- poetry.lock | 335 +++++++++++++++++++++++++++---------------------- pyproject.toml | 9 +- 2 files changed, 194 insertions(+), 150 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7c6fa0e76..678708156 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,15 +1,16 @@ -# This file is automatically @generated by Poetry 2.0.1 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]] @@ -26,19 +27,20 @@ files = [ ] [[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" @@ -83,7 +85,7 @@ 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)"] @@ -94,6 +96,7 @@ 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"}, @@ -106,6 +109,7 @@ description = "The Real First Universal Charset Detector. Open, modern and activ 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"}, @@ -131,16 +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 = "sys_platform == \"win32\"", lint = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} +markers = {docs = "python_version >= \"3.11\" and sys_platform == \"win32\"", lint = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} [[package]] name = "coverage" @@ -206,7 +210,7 @@ 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" @@ -266,14 +270,15 @@ files = [ [[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]] @@ -316,6 +321,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"}, @@ -328,32 +334,12 @@ 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" -groups = ["docs"] -markers = "python_version < \"3.10\"" -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" @@ -388,6 +374,7 @@ 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"}, @@ -406,6 +393,7 @@ 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"}, @@ -484,6 +472,7 @@ 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" @@ -555,18 +544,19 @@ files = [ [[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" @@ -713,18 +703,6 @@ 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 = "*" -groups = ["docs"] -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" @@ -732,6 +710,7 @@ 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"}, @@ -747,6 +726,23 @@ 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" @@ -754,114 +750,140 @@ description = "This package provides 29 stemmers for 28 languages generated from 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" @@ -869,6 +891,7 @@ description = "A sphinx extension which renders display math in HTML via JavaScr 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"}, @@ -879,46 +902,80 @@ 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 = ["docs", "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"}, ] [[package]] @@ -941,14 +998,15 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 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"}, ] [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"] +brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -1212,24 +1270,7 @@ winrt-runtime = "3.1.0" [package.extras] 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)"] -[[package]] -name = "zipp" -version = "3.19.1" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -markers = "python_version < \"3.10\"" -files = [ - {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, - {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, -] - -[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)"] - [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "28096101bee9ebdfb7c475df12670b3b2e3c3224071f3370f0b9b431602be765" +content-hash = "9d339c9c074fa90012b04438993916d8c712d9aee4f7e1d70a8a86cbe5580984" diff --git a/pyproject.toml b/pyproject.toml index 2e2c3f0f5..f55dab243 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,10 +41,13 @@ pyobjc-framework-libdispatch = { version = ">=10.3", markers = "platform_system= "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" } +tomli = ">=2.2.1" [tool.poetry.group.lint.dependencies] black = ">=24.3,<25.0" From 32f6651a9513d2dd28c1cc6041a4c211b17bb68d Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 10 May 2025 12:32:22 -0500 Subject: [PATCH 027/113] corebluetooth: add type hints for new notification discriminator --- bleak/__init__.py | 11 +++++++++- .../corebluetooth/PeripheralDelegate.py | 7 +++++-- bleak/backends/corebluetooth/client.py | 21 ++++++++++++++++--- docs/backends/macos.rst | 4 +++- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 15dd74e65..b850f66c7 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -62,6 +62,7 @@ if TYPE_CHECKING: from .backends.bluezdbus.scanner import BlueZScannerArgs + from .backends.corebluetooth.client import CBStartNotifyArgs from .backends.corebluetooth.scanner import CBScannerArgs from .backends.winrt.client import WinRTClientArgs @@ -813,6 +814,8 @@ async def start_notify( callback: Callable[ [BleakGATTCharacteristic, bytearray], Union[None, Awaitable[None]] ], + *, + cb: CBStartNotifyArgs = {}, **kwargs, ) -> None: """ @@ -836,11 +839,15 @@ 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. .. versionchanged:: 0.18 The first argument of the callback is now a :class:`BleakGATTCharacteristic` instead of an ``int``. + .. versionchanged:: unreleased + Added the ``cb`` parameter. """ if not self.is_connected: raise BleakError("Not connected") @@ -863,7 +870,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] diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index 9726a4295..fde242e33 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -35,6 +35,9 @@ # logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) + +NotificationDiscriminator = Callable[[bytes], bool] + CBPeripheralDelegate = objc.protocolNamed("CBPeripheralDelegate") CBCharacteristicWriteType = NewType("CBCharacteristicWriteType", int) @@ -72,7 +75,7 @@ def initWithPeripheral_( self._characteristic_notify_change_futures: Dict[int, asyncio.Future] = {} self._characteristic_notify_callbacks: Dict[int, NotifyCallback] = {} self._characteristic_notification_discriminators: Dict[ - int, Callable[[bytearray], bool] + int, Optional[NotificationDiscriminator] ] = {} self._read_rssi_futures: Dict[NSUUID, asyncio.Future] = {} @@ -221,7 +224,7 @@ async def start_notifications( self, characteristic: CBCharacteristic, callback: NotifyCallback, - notification_discriminator: Optional[Callable[[bytes], bool]] = None, + notification_discriminator: Optional[NotificationDiscriminator] = None, ) -> None: c_handle = characteristic.handle() if c_handle in self._characteristic_notify_callbacks: diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 1bc033a71..bc7f554a6 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -8,7 +8,7 @@ import logging import sys import uuid -from typing import Any, Dict, Optional, Set, Union +from typing import Optional, Set, TypedDict, Union if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -37,7 +37,7 @@ from .CentralManagerDelegate import CentralManagerDelegate from .characteristic import BleakGATTCharacteristicCoreBluetooth from .descriptor import BleakGATTDescriptorCoreBluetooth -from .PeripheralDelegate import PeripheralDelegate +from .PeripheralDelegate import NotificationDiscriminator, PeripheralDelegate from .scanner import BleakScannerCoreBluetooth from .service import BleakGATTServiceCoreBluetooth from .utils import cb_uuid_to_str @@ -45,6 +45,21 @@ logger = logging.getLogger(__name__) +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. + """ + + class BleakClientCoreBluetooth(BaseBleakClient): """CoreBluetooth class interface for BleakClient @@ -358,7 +373,7 @@ async def start_notify( characteristic: BleakGATTCharacteristic, callback: NotifyCallback, *, - cb: Dict[str, Any] = {}, + cb: CBStartNotifyArgs, **kwargs, ) -> None: """ diff --git a/docs/backends/macos.rst b/docs/backends/macos.rst index 67a0b1897..25d37d862 100644 --- a/docs/backends/macos.rst +++ b/docs/backends/macos.rst @@ -32,6 +32,8 @@ Pairing There is no pairing functionality implemented in macOS right now, since it does not seem to be any explicit pairing methods in CoreBluetooth. +.. _cb-notification-discriminator: + Notifications ^^^^^^^^^^^^^ CoreBluetooth does not differentiate between data from a notification and data from a read. @@ -39,7 +41,7 @@ This can cause confusion in cases where a device may send a notification message as a signal that the characteristic needs to be read again. Bleak has can accept a ``notification_discriminator`` callback in the ``cb`` dict parameter that is -passed to the :meth:`BleakClient.start_notify` method that can differentiate between these types of data. +passed to the :meth:`bleak.BleakClient.start_notify` method that can differentiate between these types of data. .. code-block:: python From 3980bb5e26014fe176e5ed08433e1d0a48bff5d1 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 10 May 2025 12:39:31 -0500 Subject: [PATCH 028/113] docs/macos: elaborate on pairing --- docs/backends/macos.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/backends/macos.rst b/docs/backends/macos.rst index 25d37d862..ade5fb631 100644 --- a/docs/backends/macos.rst +++ b/docs/backends/macos.rst @@ -32,6 +32,14 @@ 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 From 4b926d86a487af7d7aae93e29276352025375386 Mon Sep 17 00:00:00 2001 From: Niflheim-uk <141544683+Niflheim-uk@users.noreply.github.com> Date: Fri, 16 May 2025 01:51:03 +0100 Subject: [PATCH 029/113] Att error 15 name fix (#1746) Corrected code 15 from Bluetooth core specification --- CHANGELOG.rst | 1 + bleak/exc.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b65a6ffaa..8202a97dd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,7 @@ Fixed * 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. * Fixed getting notifications from devices connected to BLE adapters with index >9 (hci10, hci11, ...) +* Fixed ATT error code 15 description to "Insufficient Encryption". Merged #1746. Removed ------- diff --git a/bleak/exc.py b/bleak/exc.py index d03d20358..567054113 100644 --- a/bleak/exc.py +++ b/bleak/exc.py @@ -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", From 8031c376edc9651f7c001feac3ba12c6c1a53066 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 19:52:04 -0500 Subject: [PATCH 030/113] CHANGELOG: remove duplicate heading --- CHANGELOG.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8202a97dd..c8fc42b0e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,8 +9,6 @@ and this project adheres to `Semantic Versioning Date: Thu, 15 May 2025 20:03:01 -0500 Subject: [PATCH 031/113] pyproject update jinja2 --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 678708156..011ee3da7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -369,15 +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] From 6db7b6b4c6da996d3f6f77521cbb5340b41032a1 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 20:10:20 -0500 Subject: [PATCH 032/113] bleak: undeprecate response=None arg to BleakClient.write_gatt_char() Fixes: https://github.com/hbldh/bleak/issues/1743 --- CHANGELOG.rst | 3 +-- bleak/__init__.py | 14 +++----------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c8fc42b0e..57fb8edd8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,8 +16,7 @@ Added Changed ------- -* Made `response` argument of class ``BleakClient.write_gatt_char()`` explicitly optional. #1730 -* Deprecated omitting the ``response`` argument. +* Made `response` argument of class ``BleakClient.write_gatt_char()`` explicitly optional. Fixes #1730. * Updated Poetry build system version to ``>=2.0``. * Log to stderr instead of stdout when ``BLEAK_LOGGING`` is enabled. * Updated ``winrt`` backend to use PyWinRT >= 3.1. diff --git a/bleak/__init__.py b/bleak/__init__.py index b850f66c7..f512f4d37 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -777,10 +777,6 @@ async def write_gatt_char( .. versionchanged:: 0.21 The default behavior when ``response=`` is omitted was changed. - .. versionchanged:: unreleased - Omitting the ``response=`` argument is deprecated and may be required - in a future release. - Example:: MY_CHAR_UUID = "1234" @@ -796,14 +792,10 @@ async def write_gatt_char( raise BleakCharacteristicNotFoundError(char_specifier) 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. - warn( - "Omitting the response argument is not recommended." - "Legacy usage may be not allowed in the future.", - DeprecationWarning, - stacklevel=2, - ) + # 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) From 7e960d8759f722b6cb918a689abf01c0eac8b40d Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 20:22:08 -0500 Subject: [PATCH 033/113] remove deprecated parameters There were a few undocumented parameters that have been deprecated for 3 years. Remove them. --- CHANGELOG.rst | 1 + bleak/backends/bluezdbus/scanner.py | 12 +----------- bleak/backends/winrt/client.py | 10 +--------- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 57fb8edd8..f3dd2810e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,6 +33,7 @@ Fixed Removed ------- * Removed support for Python 3.8. The minimum supported version is now Python 3.9. +* Removed deprecated parameters. `0.22.3`_ (2024-10-05) ====================== diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 186e7d42b..e178df0cf 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -1,6 +1,5 @@ import logging from typing import Callable, Coroutine, Dict, List, Literal, Optional, TypedDict -from warnings import warn from dbus_fast import Variant @@ -140,16 +139,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) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index a39176f56..6ba289b47 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -9,7 +9,6 @@ import logging import sys import uuid -import warnings from ctypes import WinError from typing import ( Any, @@ -210,16 +209,9 @@ def __init__( self._session: Optional[GattSession] = None self._notification_callbacks: Dict[int, EventRegistrationToken] = {} - 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, - ) - # 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 From 57bb701661b19172f33d95c87ec6034aaf5c0f01 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 20:31:41 -0500 Subject: [PATCH 034/113] bleak: remove deprecated methods These methods have been deprecated for 3 years, so we can remove them now. --- CHANGELOG.rst | 2 +- bleak/__init__.py | 129 +------------------------------------------ docs/api/client.rst | 7 --- docs/api/index.rst | 7 --- docs/api/scanner.rst | 8 --- 5 files changed, 4 insertions(+), 149 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f3dd2810e..9f139e1fd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,7 +33,7 @@ Fixed Removed ------- * Removed support for Python 3.8. The minimum supported version is now Python 3.9. -* Removed deprecated parameters. +* Removed deprecated parameters and methods. `0.22.3`_ (2024-10-05) ====================== diff --git a/bleak/__init__.py b/bleak/__init__.py index f512f4d37..99e2788fd 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -32,7 +32,6 @@ Union, overload, ) -from warnings import warn if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -167,38 +166,6 @@ async def __aexit__( ) -> 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() @@ -207,25 +174,6 @@ 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]: @@ -352,24 +300,6 @@ def discovered_devices_and_advertisement_data( """ 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 - @classmethod async def find_device_by_address( cls, device_identifier: str, timeout: float = 10.0, **kwargs: Unpack[ExtraArgs] @@ -593,28 +523,6 @@ async def __aexit__( # 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: """Connect to the specified GATT server. @@ -678,24 +586,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: """ @@ -911,21 +801,6 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: await self._backend.write_gatt_descriptor(handle, data) -# 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) - - def cli() -> None: import argparse @@ -938,7 +813,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/docs/api/client.rst b/docs/api/client.rst index bc7e079e4..8ca94304c 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -111,10 +111,3 @@ required. .. 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..ce8d11fc3 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -45,10 +45,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 From 734d589f39b92a839ed23a38eea3df73a335a2b8 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 20:38:15 -0500 Subject: [PATCH 035/113] don't allow calling is_connected as a method Calling is_connected has been deprecated for 3 years, so we can drop the deprecated workaround. --- bleak/backends/bluezdbus/client.py | 4 +--- bleak/backends/client.py | 24 ------------------------ bleak/backends/corebluetooth/client.py | 2 +- bleak/backends/winrt/client.py | 2 +- 4 files changed, 3 insertions(+), 29 deletions(-) diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 0181bc064..818e9974d 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -580,9 +580,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 diff --git a/bleak/backends/client.py b/bleak/backends/client.py index 789a996f0..54c0dcd81 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -6,13 +6,11 @@ """ import abc -import asyncio import os import platform import sys import uuid from typing import Callable, Optional, Type, Union -from warnings import warn if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -125,28 +123,6 @@ 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 diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index bc7f554a6..00b21b996 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -173,7 +173,7 @@ async def disconnect(self) -> bool: @property 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 diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 6ba289b47..7c1bbf1ad 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -552,7 +552,7 @@ 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 From 9af83f7aa149bb6926d921dbbe267404560652f3 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 21:02:17 -0500 Subject: [PATCH 036/113] Remove deprecated properties from BLEDevice. These have been deprecated for 3 years so can be removed. --- bleak/backends/device.py | 43 ++------------------------------------- bleak/backends/scanner.py | 16 +-------------- 2 files changed, 3 insertions(+), 56 deletions(-) diff --git a/bleak/backends/device.py b/bleak/backends/device.py index 5ce5c89a2..540f823a3 100644 --- a/bleak/backends/device.py +++ b/bleak/backends/device.py @@ -9,7 +9,6 @@ from typing import Any, Optional -from warnings import warn class BLEDevice: @@ -17,11 +16,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): #: 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,42 +27,6 @@ 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 - def __str__(self): return f"{self.address}: {self.name}" diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index eb0d71f06..f246de9b8 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -262,26 +262,12 @@ 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.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) From 28e4f53b1d51d818f52d777fa9e839e367909281 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 21:23:28 -0500 Subject: [PATCH 037/113] change connect/disconnect/pair/unpair method returns to None For a long time, Bleak has raised an exception instead of returning False and always returns True on success. Still, having a bool return leads people to unnecessarily check for success. To avoid this, change the return type to None. --- CHANGELOG.rst | 1 + bleak/__init__.py | 35 +++++++++---------- bleak/backends/bluezdbus/client.py | 48 ++++---------------------- bleak/backends/client.py | 19 +++------- bleak/backends/corebluetooth/client.py | 39 ++++++++------------- bleak/backends/p4android/client.py | 44 ++++++----------------- bleak/backends/winrt/client.py | 39 ++++----------------- 7 files changed, 61 insertions(+), 164 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9f139e1fd..082af5359 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,7 @@ Changed * Log to stderr instead of stdout when ``BLEAK_LOGGING`` is enabled. * Updated ``winrt`` backend to use PyWinRT >= 3.1. * Added ``notification_discriminator`` parameter to start_notify on CoreBluetooth backend +* Changed return type of ``connect()``, ``disconnect()``, ``pair()`` and ``unpair()`` methods to ``None``. Fixed ----- diff --git a/bleak/__init__.py b/bleak/__init__.py index 99e2788fd..a960d3db5 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -523,28 +523,26 @@ async def __aexit__( # Connectivity methods - async def connect(self, **kwargs) -> bool: + async def connect(self, **kwargs) -> None: """Connect to the specified GATT server. Args: **kwargs: For backwards compatibility - should not be used. - Returns: - Always returns ``True`` for backwards compatibility. - + .. versionchanged:: unreleased + No longer returns ``True``. Instead, the return type is ``None``. """ - return await self._backend.connect(self._pair_before_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:: unreleased + 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, **kwargs) -> None: """ Pair with the specified GATT server. @@ -553,13 +551,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:: unreleased + 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. @@ -568,10 +565,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:: unreleased + 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: diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 818e9974d..c693cd1af 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -107,15 +107,12 @@ def __init__( async def connect( self, pair: bool, dangerous_use_bleak_cache: bool = False, **kwargs - ) -> bool: + ) -> 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 @@ -345,7 +342,6 @@ async def disconnect_device() -> None: ) stack.pop_all() - return True @staticmethod async def _disconnect_monitor( @@ -411,12 +407,9 @@ def _cleanup_all(self) -> None: # Reset all stored services. self.services = None - async def disconnect(self) -> bool: + 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 @@ -428,7 +421,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 @@ -457,17 +450,11 @@ 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: + 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. - """ # See if it is already paired. reply = await self._bus.call( @@ -483,7 +470,7 @@ async def pair(self, *args, **kwargs) -> bool: 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( @@ -510,27 +497,8 @@ async def pair(self, *args, **kwargs) -> bool: ) 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. - - """ + 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() @@ -570,8 +538,6 @@ async def unpair(self) -> bool: ) from e raise - return True - @property def is_connected(self) -> bool: """Check connection status between this client and the server. diff --git a/bleak/backends/client.py b/bleak/backends/client.py index 54c0dcd81..3bb27dd16 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -76,7 +76,7 @@ def set_disconnected_callback( self._disconnected_callback = callback @abc.abstractmethod - async def connect(self, pair: bool, **kwargs) -> bool: + async def connect(self, pair: bool, **kwargs) -> None: """Connect to the specified GATT server. Args: @@ -85,30 +85,21 @@ async def connect(self, pair: bool, **kwargs) -> bool: Backends that can't implement this should make an appropriate log message and ignore the parameter. - - Returns: - Boolean representing connection status. - """ 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, **kwargs) -> 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() diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 00b21b996..14b64a771 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -99,15 +99,11 @@ def __init__( def __str__(self) -> str: return "BleakClientCoreBluetooth ({})".format(self.address) - async def connect(self, pair: bool, **kwargs) -> bool: + async def connect(self, pair: bool, **kwargs) -> 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.") @@ -156,20 +152,16 @@ def disconnect_callback() -> None: # Now get services await self.get_services() - return True - - async def disconnect(self) -> bool: + async def disconnect(self) -> None: """Disconnect from the peripheral device""" if ( self._peripheral is None or self._peripheral.state() != CBPeripheralStateConnected ): - return True + return await self._central_manager_delegate.disconnect(self._peripheral) - return True - @property def is_connected(self) -> bool: """Checks for current active connection""" @@ -192,32 +184,31 @@ def mtu_size(self) -> int: + 3 ) - async def pair(self, *args, **kwargs) -> bool: + async def pair(self, *args, **kwargs) -> 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: + 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.") diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index 360e3e534..b9a46842c 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -57,13 +57,8 @@ def __del__(self): # Connectivity methods - async def connect(self, pair: bool, **kwargs) -> bool: - """Connect to the specified GATT server. - - Returns: - Boolean representing connection status. - - """ + 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") @@ -125,22 +120,15 @@ async def connect(self, pair: bool, **kwargs) -> bool: pass raise - return True - - async def disconnect(self) -> bool: - """Disconnect from the specified GATT server. - - Returns: - Boolean representing if device is disconnected. - - """ + 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: @@ -162,17 +150,11 @@ async def disconnect(self) -> bool: # Reset all stored services. self.services = None - return True - - async def pair(self, *args, **kwargs) -> bool: + 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() @@ -204,28 +186,22 @@ 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. - - """ + 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 def is_connected(self) -> bool: diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 7c1bbf1ad..98f48696b 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -245,15 +245,11 @@ async def _create_requester(self, bluetooth_address: int) -> BluetoothLEDevice: ) return requester - async def connect(self, pair: bool, **kwargs) -> bool: + async def connect(self, pair: bool, **kwargs) -> 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) @@ -489,15 +485,8 @@ 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. - - Returns: - Boolean representing if device is disconnected. - - """ + async def disconnect(self) -> None: + """Disconnect from the specified GATT server.""" logger.debug("Disconnecting from BLE device...") assert self.services @@ -542,8 +531,6 @@ async def disconnect(self) -> bool: finally: self._session_closed_events.remove(event) - return True - @property def is_connected(self) -> bool: """Check connection status between this client and the server. @@ -565,7 +552,7 @@ def mtu_size(self) -> int: async def pair( self, protection_level: Optional[DevicePairingProtectionLevel] = None, **kwargs - ) -> bool: + ) -> None: """Attempts to pair with the device. Keyword Args: @@ -575,10 +562,6 @@ async def pair( 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. - """ assert self._requester @@ -588,12 +571,11 @@ async def pair( ) if not device_information.pairing.can_pair: - logging.debug("Device does not support pairing. Skipping pairing.") - return False + raise BleakError("Device does not support pairing") if device_information.pairing.is_paired: logging.debug("Device is already paired. Skipping pairing.") - return True + return # Currently only supporting Just Works solutions... ceremony = DevicePairingKinds.CONFIRM_ONLY @@ -630,16 +612,11 @@ def handler( "Paired to device with protection level %r.", pairing_result.protection_level_used, ) - return True - async def unpair(self) -> bool: + async def unpair(self) -> None: """Attempts to unpair from the device. N.B. unpairing also leads to disconnection in the Windows backend. - - Returns: - Boolean on whether the unparing was successful. - """ device = await self._create_requester( self._device_info @@ -660,8 +637,6 @@ async def unpair(self) -> bool: finally: device.close() - return True - # GATT services methods async def get_services( From b2ddaa13fd225cd8a8005d823629b49875fe2d06 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 21:53:01 -0500 Subject: [PATCH 038/113] Drop support for macOS < 10.13 This removes a bunch of conditionals that had workarounds for older macOS versions. --- .github/pull_request_template.md | 2 +- CHANGELOG.rst | 1 + CONTRIBUTING.rst | 2 +- README.rst | 2 +- .../corebluetooth/CentralManagerDelegate.py | 53 +++++++------------ .../corebluetooth/PeripheralDelegate.py | 50 ++++------------- bleak/backends/corebluetooth/scanner.py | 6 +-- docs/backends/index.rst | 2 +- docs/index.rst | 2 +- 9 files changed, 37 insertions(+), 83 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a06f44385..d74e17c57 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,7 +9,7 @@ Before you submit a pull request, check that it meets these guidelines: 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 + - 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/CHANGELOG.rst b/CHANGELOG.rst index 082af5359..728a49fb1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,6 +35,7 @@ Removed ------- * Removed support for Python 3.8. The minimum supported version is now Python 3.9. * Removed deprecated parameters and methods. +* Removed support for macOS < 10.13. `0.22.3`_ (2024-10-05) ====================== diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 30847c766..c0bebbaa0 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -105,7 +105,7 @@ Before you submit a pull request, check that it meets these guidelines: 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 + - 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..b238bbbeb 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ 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 +* 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/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 6b8766414..1c2ce2d27 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -93,25 +93,22 @@ 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 @@ -129,29 +126,19 @@ async def start_scan(self, service_uuids: Optional[List[str]]) -> 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( diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index fde242e33..51fa52152 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -590,7 +590,16 @@ 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: NSNumber, + 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 @@ -619,42 +628,3 @@ def 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/scanner.py b/bleak/backends/corebluetooth/scanner.py index 866b5c442..06f7c394e 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -178,8 +178,4 @@ def set_scanning_filter(self, **kwargs) -> None: @property def is_scanning(self): - # TODO: Evaluate if newer macOS than 10.11 has isScanning. - try: - return self._manager.isScanning_ - except Exception: - return None + return self._manager.isScanning_ diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 2afbb68d1..04a4294b6 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -5,7 +5,7 @@ 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 +* 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/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. From 6dc66ce3b75d3f533bacdc8d691efd8149f0d816 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 22:20:37 -0500 Subject: [PATCH 039/113] Drop support for BlueZ < 5.55 This is the version of BlueZ in Debian Buster which is currently considered oldstable. --- .github/pull_request_template.md | 2 +- CHANGELOG.rst | 1 + CONTRIBUTING.rst | 2 +- README.rst | 2 +- bleak/backends/bluezdbus/client.py | 99 ++++--------------- bleak/backends/bluezdbus/version.py | 14 +-- docs/backends/index.rst | 2 +- docs/backends/linux.rst | 12 --- .../bleak/backends/bluezdbus/test_version.py | 25 +---- 9 files changed, 32 insertions(+), 127 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d74e17c57..00060d471 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,7 +8,7 @@ Before you submit a pull request, check that it meets these guidelines: guidelines in that document. 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 + - 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, diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 728a49fb1..06cc09071 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,6 +36,7 @@ Removed * Removed support for Python 3.8. The minimum supported version is now Python 3.9. * Removed deprecated parameters and methods. * Removed support for macOS < 10.13. +* Removed support for BlueZ < 5.55. `0.22.3`_ (2024-10-05) ====================== diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c0bebbaa0..dae1ff871 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -104,7 +104,7 @@ Before you submit a pull request, check that it meets these guidelines: guidelines in that document. 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 + - 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, diff --git a/README.rst b/README.rst index b238bbbeb..da1fe095b 100644 --- a/README.rst +++ b/README.rst @@ -46,7 +46,7 @@ Features -------- * Supports Windows 10, version 16299 (Fall Creators Update) or greater -* Supports Linux distributions with BlueZ >= 5.43 +* 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 diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index c693cd1af..6b874eed9 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -126,7 +126,7 @@ async def connect( 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) @@ -688,35 +688,7 @@ async def read_gatt_char( 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 - ): + if str(char_specifier) == "00002a00-0000-1000-8000-00805f9b34fb": # 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()) @@ -821,63 +793,34 @@ async def write_gatt_char( 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, 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", diff --git a/bleak/backends/bluezdbus/version.py b/bleak/backends/bluezdbus/version.py index 40842cb96..f15d89299 100644 --- a/bleak/backends/bluezdbus/version.py +++ b/bleak/backends/bluezdbus/version.py @@ -25,10 +25,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 +39,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/docs/backends/index.rst b/docs/backends/index.rst index 04a4294b6..043219772 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -4,7 +4,7 @@ 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) +* 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. 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/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 From d932d478d85be3bcccd44ee485cacadd8c0bccd3 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 22:49:23 -0500 Subject: [PATCH 040/113] github: add bot to close stale issues May issues are 3rd-party or need more info. We don't aggressively close these, but there is no reason to leave them open forever. --- .github/workflows/stale_issues.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/stale_issues.yml diff --git a/.github/workflows/stale_issues.yml b/.github/workflows/stale_issues.yml new file mode 100644 index 000000000..c0ab06bfc --- /dev/null +++ b/.github/workflows/stale_issues.yml @@ -0,0 +1,14 @@ +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' From b988c1a8cfd940dcf50a149085520dd1e25ff577 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 22:59:14 -0500 Subject: [PATCH 041/113] github: only build and test if code changes This is no need to build and run tests if no code changes. --- .github/workflows/build_and_test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index fb9ef8aa7..d495176cf 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: From 050acfc9a18bb2bf91372310875f18be94431688 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 15 May 2025 23:21:06 -0500 Subject: [PATCH 042/113] docs: update RPi troubleshooting section Fixes: https://github.com/hbldh/bleak/issues/1747 --- docs/troubleshooting.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 ---------- From a50c9862ee29d5fa0213f6c0f60e703f7201619b Mon Sep 17 00:00:00 2001 From: Alan Fischer Date: Fri, 16 May 2025 09:56:55 -0600 Subject: [PATCH 043/113] Update macos.rst Remove the can / has wording in place of can. --- docs/backends/macos.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backends/macos.rst b/docs/backends/macos.rst index ade5fb631..410781400 100644 --- a/docs/backends/macos.rst +++ b/docs/backends/macos.rst @@ -48,7 +48,7 @@ CoreBluetooth does not differentiate between data from a notification and data f 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 has can accept a ``notification_discriminator`` callback in the ``cb`` dict parameter that is +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 From 745e014c5bef23f843b5ee80612f5b56e3ae0b2a Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 16 May 2025 20:05:10 -0500 Subject: [PATCH 044/113] typing: use Python 3.9 list[T] instead of List[T] --- bleak/__init__.py | 9 +++---- bleak/backends/bluezdbus/characteristic.py | 8 +++--- bleak/backends/bluezdbus/defs.py | 26 +++++++++---------- bleak/backends/bluezdbus/manager.py | 11 ++++---- bleak/backends/bluezdbus/scanner.py | 8 +++--- bleak/backends/bluezdbus/service.py | 4 +-- bleak/backends/characteristic.py | 6 ++--- .../corebluetooth/CentralManagerDelegate.py | 4 +-- .../backends/corebluetooth/characteristic.py | 10 +++---- bleak/backends/corebluetooth/scanner.py | 4 +-- bleak/backends/corebluetooth/service.py | 6 ++--- bleak/backends/p4android/characteristic.py | 6 ++--- bleak/backends/p4android/scanner.py | 4 +-- bleak/backends/p4android/service.py | 4 +-- bleak/backends/scanner.py | 11 ++++---- bleak/backends/service.py | 4 +-- bleak/backends/winrt/characteristic.py | 6 ++--- bleak/backends/winrt/client.py | 7 +++-- bleak/backends/winrt/scanner.py | 4 +-- bleak/backends/winrt/service.py | 4 +-- 20 files changed, 68 insertions(+), 78 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index a960d3db5..6801fb04b 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -22,7 +22,6 @@ Callable, Dict, Iterable, - List, Literal, Optional, Set, @@ -133,7 +132,7 @@ 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 = {}, @@ -205,7 +204,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 @@ -235,7 +234,7 @@ class ExtraArgs(TypedDict, total=False): @classmethod async def discover( cls, timeout: float = 5.0, *, return_adv: Literal[False] = False, **kwargs - ) -> List[BLEDevice]: ... + ) -> list[BLEDevice]: ... @overload @classmethod @@ -275,7 +274,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. diff --git a/bleak/backends/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py index 7808c26ef..2e8d406a6 100644 --- a/bleak/backends/bluezdbus/characteristic.py +++ b/bleak/backends/bluezdbus/characteristic.py @@ -1,4 +1,4 @@ -from typing import Callable, List, Union +from typing import Callable, Union from uuid import UUID from ..characteristic import BleakGATTCharacteristic @@ -21,7 +21,7 @@ def __init__( super(BleakGATTCharacteristicBlueZDBus, self).__init__( obj, max_write_without_response_size ) - self.__descriptors = [] + self.__descriptors: list[BleakGATTDescriptor] = [] self.__path = object_path self.__service_uuid = service_uuid self.__service_handle = service_handle @@ -48,7 +48,7 @@ def uuid(self) -> str: return self.obj.get("UUID") @property - def properties(self) -> List[str]: + def properties(self) -> list[str]: """Properties of this characteristic Returns the characteristics `Flags` present in the DBus API. @@ -56,7 +56,7 @@ def properties(self) -> List[str]: return self.obj["Flags"] @property - def descriptors(self) -> List[BleakGATTDescriptor]: + def descriptors(self) -> list[BleakGATTDescriptor]: """List of descriptors for this service""" return self.__descriptors diff --git a/bleak/backends/bluezdbus/defs.py b/bleak/backends/bluezdbus/defs.py index 18ed0c331..0dbc87de0 100644 --- a/bleak/backends/bluezdbus/defs.py +++ b/bleak/backends/bluezdbus/defs.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Literal, Tuple, TypedDict +from typing import Dict, Literal, Tuple, TypedDict # DBus Interfaces OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager" @@ -37,10 +37,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 +53,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 +82,7 @@ class Device1(TypedDict): Icon: str Class: int Appearance: int - UUIDs: List[str] + UUIDs: list[str] Paired: bool Bonded: bool Connected: bool @@ -109,7 +109,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,7 +120,7 @@ class GattCharacteristic1(TypedDict): WriteAcquired: bool NotifyAcquired: bool Notifying: bool - Flags: List[ + Flags: list[ Literal[ "broadcast", "read", @@ -151,7 +151,7 @@ class GattDescriptor1(TypedDict): UUID: str Characteristic: str Value: bytes - Flags: List[ + Flags: list[ Literal[ "read", "write", diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index 494676fea..f64ee164e 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -16,7 +16,6 @@ Callable, Coroutine, Dict, - List, MutableMapping, NamedTuple, Optional, @@ -178,10 +177,10 @@ def __init__(self): # map of characteristic d-bus object paths to set of descriptor d-bus object paths 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_removed_callbacks: list[DeviceRemovedCallbackAndState] = [] self._device_watchers: Dict[str, Set[DeviceWatcher]] = {} self._condition_callbacks: Dict[str, Set[DeviceConditionCallback]] = {} self._services_cache: Dict[str, BleakGATTServiceCollection] = {} @@ -474,7 +473,7 @@ 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]: @@ -895,10 +894,10 @@ def _parse_msg(self, message: Message) -> None: # type hints obj_path: str interfaces_and_props: Dict[str, Dict[str, Variant]] - interfaces: List[str] + interfaces: list[str] interface: str changed: Dict[str, Variant] - invalidated: List[str] + invalidated: list[str] if message.member == "InterfacesAdded": obj_path, interfaces_and_props = message.body diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index e178df0cf..8f68a3b0c 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -1,5 +1,5 @@ import logging -from typing import Callable, Coroutine, Dict, List, Literal, Optional, TypedDict +from typing import Callable, Coroutine, Dict, Literal, Optional, TypedDict from dbus_fast import Variant @@ -21,7 +21,7 @@ class BlueZDiscoveryFilters(TypedDict, total=False): https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst#void-setdiscoveryfilterdict-filter """ - UUIDs: List[str] + UUIDs: list[str] """ Filter by service UUIDs, empty means match _any_ UUID. @@ -79,7 +79,7 @@ class BlueZScannerArgs(TypedDict, total=False): Only used for active scanning. """ - or_patterns: List[OrPatternLike] + or_patterns: list[OrPatternLike] """ Or patterns to pass to the AdvertisementMonitor1 D-Bus interface. @@ -113,7 +113,7 @@ 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, diff --git a/bleak/backends/bluezdbus/service.py b/bleak/backends/bluezdbus/service.py index a1a8d3c27..1febdcc2c 100644 --- a/bleak/backends/bluezdbus/service.py +++ b/bleak/backends/bluezdbus/service.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any from ..service import BleakGATTService from .characteristic import BleakGATTCharacteristicBlueZDBus @@ -25,7 +25,7 @@ def handle(self) -> int: return self.__handle @property - def characteristics(self) -> List[BleakGATTCharacteristicBlueZDBus]: + def characteristics(self) -> list[BleakGATTCharacteristicBlueZDBus]: """List of characteristics for this service""" return self.__characteristics diff --git a/bleak/backends/characteristic.py b/bleak/backends/characteristic.py index eca52d5ee..f551acba2 100644 --- a/bleak/backends/characteristic.py +++ b/bleak/backends/characteristic.py @@ -7,7 +7,7 @@ """ import abc import enum -from typing import Any, Callable, List, Union +from typing import Any, Callable, Union from uuid import UUID from ..uuids import uuidstr_to_str @@ -76,7 +76,7 @@ def description(self) -> str: @property @abc.abstractmethod - def properties(self) -> List[str]: + def properties(self) -> list[str]: """Properties of this characteristic""" raise NotImplementedError() @@ -113,7 +113,7 @@ def max_write_without_response_size(self) -> int: @property @abc.abstractmethod - def descriptors(self) -> List[BleakGATTDescriptor]: + def descriptors(self) -> list[BleakGATTDescriptor]: """List of descriptors for this service""" raise NotImplementedError() diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 1c2ce2d27..4de053edf 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -10,7 +10,7 @@ import logging import sys import threading -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout @@ -113,7 +113,7 @@ def __del__(self) -> None: # User defined functions @objc.python_method - async def start_scan(self, service_uuids: Optional[List[str]]) -> None: + async def start_scan(self, service_uuids: Optional[list[str]]) -> None: service_uuids = ( NSArray.alloc().initWithArray_( list(map(CBUUID.UUIDWithString_, service_uuids)) diff --git a/bleak/backends/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py index 4bf617115..14de2e4f2 100644 --- a/bleak/backends/corebluetooth/characteristic.py +++ b/bleak/backends/corebluetooth/characteristic.py @@ -6,7 +6,7 @@ """ from enum import Enum -from typing import Callable, Dict, List, Optional, Tuple, Union +from typing import Callable, Dict, Optional, Tuple, Union from CoreBluetooth import CBCharacteristic @@ -63,9 +63,9 @@ 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.__descriptors: list[BleakGATTDescriptorCoreBluetooth] = [] # self.__props = obj.properties() - self.__props: List[str] = [ + self.__props: list[str] = [ _GattCharacteristicsPropertiesEnum[v][0] for v in [2**n for n in range(10)] if (self.obj.properties() & v) @@ -92,12 +92,12 @@ def uuid(self) -> str: return self._uuid @property - def properties(self) -> List[str]: + def properties(self) -> list[str]: """Properties of this characteristic""" return self.__props @property - def descriptors(self) -> List[BleakGATTDescriptor]: + def descriptors(self) -> list[BleakGATTDescriptor]: """List of descriptors for this service""" return self.__descriptors diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index 06f7c394e..a150d5952 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, List, Literal, Optional, TypedDict +from typing import Any, Dict, Literal, Optional, TypedDict import objc from CoreBluetooth import CBPeripheral @@ -59,7 +59,7 @@ 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, diff --git a/bleak/backends/corebluetooth/service.py b/bleak/backends/corebluetooth/service.py index 14a35a7a9..00255e153 100644 --- a/bleak/backends/corebluetooth/service.py +++ b/bleak/backends/corebluetooth/service.py @@ -1,5 +1,3 @@ -from typing import List - from CoreBluetooth import CBService from ..service import BleakGATTService @@ -12,7 +10,7 @@ class BleakGATTServiceCoreBluetooth(BleakGATTService): def __init__(self, obj: CBService): super().__init__(obj) - self.__characteristics: List[BleakGATTCharacteristicCoreBluetooth] = [] + 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()) @@ -28,7 +26,7 @@ def uuid(self) -> str: return cb_uuid_to_str(self.obj.UUID()) @property - def characteristics(self) -> List[BleakGATTCharacteristicCoreBluetooth]: + def characteristics(self) -> list[BleakGATTCharacteristicCoreBluetooth]: """List of characteristics for this service""" return self.__characteristics diff --git a/bleak/backends/p4android/characteristic.py b/bleak/backends/p4android/characteristic.py index d9f6f1912..f0d7aa26f 100644 --- a/bleak/backends/p4android/characteristic.py +++ b/bleak/backends/p4android/characteristic.py @@ -1,4 +1,4 @@ -from typing import Callable, List, Union +from typing import Callable, Union from uuid import UUID from ...exc import BleakError @@ -54,12 +54,12 @@ def uuid(self) -> str: return self.__uuid @property - def properties(self) -> List[str]: + def properties(self) -> list[str]: """Properties of this characteristic""" return self.__properties @property - def descriptors(self) -> List[BleakGATTDescriptor]: + def descriptors(self) -> list[BleakGATTDescriptor]: """List of descriptors for this service""" return self.__descriptors diff --git a/bleak/backends/p4android/scanner.py b/bleak/backends/p4android/scanner.py index fb3be74ba..ef57cdae7 100644 --- a/bleak/backends/p4android/scanner.py +++ b/bleak/backends/p4android/scanner.py @@ -4,7 +4,7 @@ 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 @@ -43,7 +43,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, ): diff --git a/bleak/backends/p4android/service.py b/bleak/backends/p4android/service.py index aab1fc278..2d49d5ca3 100644 --- a/bleak/backends/p4android/service.py +++ b/bleak/backends/p4android/service.py @@ -1,5 +1,3 @@ -from typing import List - from ..service import BleakGATTService from .characteristic import BleakGATTCharacteristicP4Android @@ -24,7 +22,7 @@ def handle(self) -> int: return self.__handle @property - def characteristics(self) -> List[BleakGATTCharacteristicP4Android]: + def characteristics(self) -> list[BleakGATTCharacteristicP4Android]: """List of characteristics for this service""" return self.__characteristics diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index f246de9b8..659cebf47 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -9,7 +9,6 @@ Coroutine, Dict, Hashable, - List, NamedTuple, Optional, Set, @@ -48,7 +47,7 @@ class AdvertisementData(NamedTuple): 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. """ @@ -75,7 +74,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: @@ -132,7 +131,7 @@ class BaseBleakScanner(abc.ABC): def __init__( self, detection_callback: Optional[AdvertisementDataCallback], - service_uuids: Optional[List[str]], + service_uuids: Optional[list[str]], ): super(BaseBleakScanner, self).__init__() @@ -146,7 +145,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 +196,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 diff --git a/bleak/backends/service.py b/bleak/backends/service.py index 09c503c99..709f5cad9 100644 --- a/bleak/backends/service.py +++ b/bleak/backends/service.py @@ -7,7 +7,7 @@ """ import abc import logging -from typing import Any, Dict, Iterator, List, Optional, Union +from typing import Any, Dict, Iterator, Optional, Union from uuid import UUID from ..exc import BleakError @@ -46,7 +46,7 @@ def description(self) -> str: @property @abc.abstractmethod - def characteristics(self) -> List[BleakGATTCharacteristic]: + def characteristics(self) -> list[BleakGATTCharacteristic]: """List of characteristics for this service""" raise NotImplementedError() diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py index 0bad1c6ad..9e6b09e8c 100644 --- a/bleak/backends/winrt/characteristic.py +++ b/bleak/backends/winrt/characteristic.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from typing import Callable, List, Union +from typing import Callable, Union from uuid import UUID from winrt.windows.devices.bluetooth.genericattributeprofile import ( @@ -104,12 +104,12 @@ def description(self) -> str: ) @property - def properties(self) -> List[str]: + def properties(self) -> list[str]: """Properties of this characteristic""" return self.__props @property - def descriptors(self) -> List[BleakGATTDescriptor]: + def descriptors(self) -> list[BleakGATTDescriptor]: """List of descriptors for this characteristic""" return self.__descriptors diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 98f48696b..74f6b2e00 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -14,7 +14,6 @@ Any, Dict, Generic, - List, Literal, Optional, Protocol, @@ -203,9 +202,9 @@ def __init__( [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._services_changed_events: list[asyncio.Event] = [] + self._session_active_events: list[asyncio.Event] = [] + self._session_closed_events: list[asyncio.Event] = [] self._session: Optional[GattSession] = None self._notification_callbacks: Dict[int, EventRegistrationToken] = {} diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index d4a14f4db..9994f517b 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -1,6 +1,6 @@ import asyncio import logging -from typing import Dict, List, Literal, NamedTuple, Optional +from typing import Dict, Literal, NamedTuple, Optional from uuid import UUID from winrt.windows.devices.bluetooth.advertisement import ( @@ -70,7 +70,7 @@ 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, ): diff --git a/bleak/backends/winrt/service.py b/bleak/backends/winrt/service.py index e387f3fd3..9bd4d4fcc 100644 --- a/bleak/backends/winrt/service.py +++ b/bleak/backends/winrt/service.py @@ -1,5 +1,3 @@ -from typing import List - from winrt.windows.devices.bluetooth.genericattributeprofile import GattDeviceService from ..service import BleakGATTService @@ -22,7 +20,7 @@ def handle(self) -> int: return self.obj.attribute_handle @property - def characteristics(self) -> List[BleakGATTCharacteristicWinRT]: + def characteristics(self) -> list[BleakGATTCharacteristicWinRT]: """List of characteristics for this service""" return self.__characteristics From eefd0fbdd0a9a63fd977fffe3cda740d12aab1f1 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 16 May 2025 20:06:39 -0500 Subject: [PATCH 045/113] typing: use python 3.9 tuple[...] instead of typing.Tuple[...] --- bleak/__init__.py | 7 +++---- bleak/backends/bluezdbus/advertisement_monitor.py | 4 ++-- bleak/backends/bluezdbus/defs.py | 4 ++-- bleak/backends/corebluetooth/characteristic.py | 4 ++-- bleak/backends/scanner.py | 5 ++--- bleak/backends/winrt/util.py | 3 +-- 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 6801fb04b..9ad7a8bd0 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -25,7 +25,6 @@ Literal, Optional, Set, - Tuple, Type, TypedDict, Union, @@ -175,7 +174,7 @@ async def stop(self) -> None: 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. @@ -240,7 +239,7 @@ async def discover( @classmethod async def discover( cls, timeout: float = 5.0, *, return_adv: Literal[True], **kwargs - ) -> Dict[str, Tuple[BLEDevice, AdvertisementData]]: ... + ) -> Dict[str, tuple[BLEDevice, AdvertisementData]]: ... @classmethod async def discover( @@ -285,7 +284,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. diff --git a/bleak/backends/bluezdbus/advertisement_monitor.py b/bleak/backends/bluezdbus/advertisement_monitor.py index 922ffab4f..85801efb8 100644 --- a/bleak/backends/bluezdbus/advertisement_monitor.py +++ b/bleak/backends/bluezdbus/advertisement_monitor.py @@ -7,7 +7,7 @@ """ import logging -from typing import Iterable, NamedTuple, Tuple, Union, no_type_check +from typing import Iterable, NamedTuple, Union, no_type_check from dbus_fast.service import PropertyAccess, ServiceInterface, dbus_property, method @@ -30,7 +30,7 @@ class OrPattern(NamedTuple): # Windows has a similar structure, so we allow generic tuple for cross-platform compatibility -OrPatternLike = Union[OrPattern, Tuple[int, AdvertisementDataType, bytes]] +OrPatternLike = Union[OrPattern, tuple[int, AdvertisementDataType, bytes]] class AdvertisementMonitor(ServiceInterface): diff --git a/bleak/backends/bluezdbus/defs.py b/bleak/backends/bluezdbus/defs.py index 0dbc87de0..02d79973c 100644 --- a/bleak/backends/bluezdbus/defs.py +++ b/bleak/backends/bluezdbus/defs.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, Literal, Tuple, TypedDict +from typing import Dict, Literal, TypedDict # DBus Interfaces OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager" @@ -53,7 +53,7 @@ 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 diff --git a/bleak/backends/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py index 14de2e4f2..246e6dff5 100644 --- a/bleak/backends/corebluetooth/characteristic.py +++ b/bleak/backends/corebluetooth/characteristic.py @@ -6,7 +6,7 @@ """ from enum import Enum -from typing import Callable, Dict, Optional, Tuple, Union +from typing import Callable, Dict, Optional, Union from CoreBluetooth import CBCharacteristic @@ -29,7 +29,7 @@ class CBCharacteristicProperties(Enum): INDICATE_ENCRYPTION_REQUIRED = 0x200 -_GattCharacteristicsPropertiesEnum: Dict[Optional[int], Tuple[str, str]] = { +_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"), diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 659cebf47..b8e15610c 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -12,7 +12,6 @@ NamedTuple, Optional, Set, - Tuple, Type, ) @@ -66,7 +65,7 @@ class AdvertisementData(NamedTuple): .. versionadded:: 0.19 """ - platform_data: Tuple + platform_data: tuple[Any, ...] """ Tuple of platform specific data. @@ -121,7 +120,7 @@ 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. diff --git a/bleak/backends/winrt/util.py b/bleak/backends/winrt/util.py index 905379448..21741ab9a 100644 --- a/bleak/backends/winrt/util.py +++ b/bleak/backends/winrt/util.py @@ -3,7 +3,6 @@ import sys from ctypes import wintypes from enum import IntEnum -from typing import Tuple from ...exc import BleakError @@ -99,7 +98,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. From 2ab0508a9ce64c8714beb63b611b0a299e438bbd Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 16 May 2025 20:13:40 -0500 Subject: [PATCH 046/113] typing: use python 3.9 dict[K, V] instead of typing.Dict --- bleak/__init__.py | 5 ++--- bleak/backends/bluezdbus/client.py | 4 ++-- bleak/backends/bluezdbus/defs.py | 8 +++---- bleak/backends/bluezdbus/manager.py | 21 +++++++++--------- bleak/backends/bluezdbus/scanner.py | 4 ++-- bleak/backends/bluezdbus/signals.py | 4 ++-- .../corebluetooth/CentralManagerDelegate.py | 12 +++++----- .../corebluetooth/PeripheralDelegate.py | 22 +++++++++---------- .../backends/corebluetooth/characteristic.py | 4 ++-- bleak/backends/corebluetooth/scanner.py | 4 ++-- bleak/backends/scanner.py | 9 ++++---- bleak/backends/service.py | 14 ++++++------ bleak/backends/winrt/client.py | 3 +-- bleak/backends/winrt/scanner.py | 4 ++-- bleak/uuids.py | 7 +++--- 15 files changed, 60 insertions(+), 65 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 9ad7a8bd0..3ab419de8 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -20,7 +20,6 @@ AsyncGenerator, Awaitable, Callable, - Dict, Iterable, Literal, Optional, @@ -239,7 +238,7 @@ async def discover( @classmethod async def discover( cls, timeout: float = 5.0, *, return_adv: Literal[True], **kwargs - ) -> Dict[str, tuple[BLEDevice, AdvertisementData]]: ... + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: ... @classmethod async def discover( @@ -284,7 +283,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. diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 6b874eed9..80e25d50b 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -8,7 +8,7 @@ import sys import warnings from contextlib import AsyncExitStack -from typing import Callable, Dict, Optional, Set, Union, cast +from typing import Callable, Optional, Set, Union, cast from uuid import UUID if sys.version_info < (3, 12): @@ -98,7 +98,7 @@ 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 diff --git a/bleak/backends/bluezdbus/defs.py b/bleak/backends/bluezdbus/defs.py index 02d79973c..ee50d7694 100644 --- a/bleak/backends/bluezdbus/defs.py +++ b/bleak/backends/bluezdbus/defs.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, Literal, TypedDict +from typing import Literal, TypedDict # DBus Interfaces OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager" @@ -95,11 +95,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 diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index f64ee164e..97dc452db 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -15,7 +15,6 @@ Any, Callable, Coroutine, - Dict, MutableMapping, NamedTuple, Optional, @@ -162,7 +161,7 @@ def __init__(self): 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() @@ -171,19 +170,19 @@ def __init__(self): # 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]] = ( 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_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: """ @@ -362,7 +361,7 @@ 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]: @@ -893,10 +892,10 @@ def _parse_msg(self, message: Message) -> None: # type hints obj_path: str - interfaces_and_props: Dict[str, Dict[str, Variant]] + interfaces_and_props: dict[str, dict[str, Variant]] interfaces: list[str] interface: str - changed: Dict[str, Variant] + changed: dict[str, Variant] invalidated: list[str] if message.member == "InterfacesAdded": diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 8f68a3b0c..5fd3ffffe 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -1,5 +1,5 @@ import logging -from typing import Callable, Coroutine, Dict, Literal, Optional, TypedDict +from typing import Callable, Coroutine, Literal, Optional, TypedDict from dbus_fast import Variant @@ -131,7 +131,7 @@ def __init__( # Discovery filters - self._filters: Dict[str, Variant] = {} + self._filters: dict[str, Variant] = {} self._filters["Transport"] = Variant("s", "le") self._filters["DuplicateData"] = Variant("b", False) diff --git a/bleak/backends/bluezdbus/signals.py b/bleak/backends/bluezdbus/signals.py index 6a2ce353f..71d44ca59 100644 --- a/bleak/backends/bluezdbus/signals.py +++ b/bleak/backends/bluezdbus/signals.py @@ -2,7 +2,7 @@ from __future__ import annotations import re -from typing import Any, Coroutine, Dict, Optional +from typing import Any, Coroutine, Optional from dbus_fast.aio.message_bus import MessageBus from dbus_fast.errors import InvalidObjectPathError @@ -132,7 +132,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: dict[str, str] = kwargs else: self.args = None diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 4de053edf..ccdec979b 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -10,7 +10,7 @@ import logging import sys import threading -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout @@ -64,13 +64,13 @@ 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] = {} - self.callbacks: Dict[ - int, Callable[[CBPeripheral, Dict[str, Any], int], None] + self.callbacks: dict[ + int, Callable[[CBPeripheral, dict[str, Any], 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] = {} self._did_update_state_event = threading.Event() self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_( diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index 51fa52152..331409bed 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -12,7 +12,7 @@ import itertools import logging import sys -from typing import Any, Callable, Dict, Iterable, NewType, Optional +from typing import Any, Callable, Iterable, NewType, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout @@ -63,22 +63,22 @@ 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] = {} + self._characteristic_descriptor_discover_futures: dict[int, asyncio.Future] = {} - self._characteristic_read_futures: Dict[int, asyncio.Future] = {} - self._characteristic_write_futures: Dict[int, asyncio.Future] = {} + self._characteristic_read_futures: dict[int, asyncio.Future] = {} + self._characteristic_write_futures: dict[int, asyncio.Future] = {} - self._descriptor_read_futures: Dict[int, asyncio.Future] = {} - self._descriptor_write_futures: Dict[int, asyncio.Future] = {} + self._descriptor_read_futures: dict[int, asyncio.Future] = {} + self._descriptor_write_futures: dict[int, asyncio.Future] = {} - self._characteristic_notify_change_futures: Dict[int, asyncio.Future] = {} - self._characteristic_notify_callbacks: Dict[int, NotifyCallback] = {} - self._characteristic_notification_discriminators: Dict[ + self._characteristic_notify_change_futures: dict[int, asyncio.Future] = {} + 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] = {} return self diff --git a/bleak/backends/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py index 246e6dff5..23fb2f38f 100644 --- a/bleak/backends/corebluetooth/characteristic.py +++ b/bleak/backends/corebluetooth/characteristic.py @@ -6,7 +6,7 @@ """ from enum import Enum -from typing import Callable, Dict, Optional, Union +from typing import Callable, Optional, Union from CoreBluetooth import CBCharacteristic @@ -29,7 +29,7 @@ class CBCharacteristicProperties(Enum): INDICATE_ENCRYPTION_REQUIRED = 0x200 -_GattCharacteristicsPropertiesEnum: Dict[Optional[int], tuple[str, str]] = { +_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"), diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index a150d5952..6daf595b0 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, Literal, Optional, TypedDict +from typing import Any, Literal, Optional, TypedDict import objc from CoreBluetooth import CBPeripheral @@ -90,7 +90,7 @@ def __init__( async def start(self) -> None: self.seen_devices = {} - def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None: + def callback(p: CBPeripheral, a: dict[str, Any], r: int) -> None: service_uuids = [ cb_uuid_to_str(u) for u in a.get("kCBAdvDataServiceUUIDs", []) diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index b8e15610c..6a642f88a 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -7,7 +7,6 @@ Any, Callable, Coroutine, - Dict, Hashable, NamedTuple, Optional, @@ -32,7 +31,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. @@ -41,7 +40,7 @@ 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. """ @@ -120,7 +119,7 @@ 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. @@ -134,7 +133,7 @@ def __init__( ): super(BaseBleakScanner, self).__init__() - self._ad_callbacks: Dict[ + self._ad_callbacks: dict[ Hashable, Callable[[BLEDevice, AdvertisementData], None] ] = {} """ diff --git a/bleak/backends/service.py b/bleak/backends/service.py index 709f5cad9..f1c2d4f5d 100644 --- a/bleak/backends/service.py +++ b/bleak/backends/service.py @@ -7,7 +7,7 @@ """ import abc import logging -from typing import Any, Dict, Iterator, Optional, Union +from typing import Any, Iterator, Optional, Union from uuid import UUID from ..exc import BleakError @@ -82,9 +82,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] @@ -103,17 +103,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/client.py b/bleak/backends/winrt/client.py index 74f6b2e00..dd6b670e7 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -12,7 +12,6 @@ from ctypes import WinError from typing import ( Any, - Dict, Generic, Literal, Optional, @@ -206,7 +205,7 @@ def __init__( self._session_active_events: list[asyncio.Event] = [] self._session_closed_events: list[asyncio.Event] = [] self._session: Optional[GattSession] = None - self._notification_callbacks: Dict[int, EventRegistrationToken] = {} + self._notification_callbacks: dict[int, EventRegistrationToken] = {} # os-specific options self._use_cached_services = winrt.get("use_cached_services") diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index 9994f517b..f63a21658 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -1,6 +1,6 @@ import asyncio import logging -from typing import Dict, Literal, NamedTuple, Optional +from typing import Literal, NamedTuple, Optional from uuid import UUID from winrt.windows.devices.bluetooth.advertisement import ( @@ -77,7 +77,7 @@ def __init__( super(BleakScannerWinRT, self).__init__(detection_callback, service_uuids) self.watcher: Optional[BluetoothLEAdvertisementWatcher] = None - self._advertisement_pairs: Dict[str, _RawAdvData] = {} + self._advertisement_pairs: dict[str, _RawAdvData] = {} self._stopped_event: Optional[asyncio.Event] = None # case insensitivity is for backwards compatibility on Windows only 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: From 3e5a596cf5d17d3a15b9db13ea0a839c683238ea Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 16 May 2025 20:24:50 -0500 Subject: [PATCH 047/113] typing: use python 3.9 set[T] instead of typing.Set --- bleak/__init__.py | 3 +-- bleak/backends/bluezdbus/client.py | 6 +++--- bleak/backends/bluezdbus/manager.py | 25 ++++++++----------------- bleak/backends/corebluetooth/client.py | 4 ++-- bleak/backends/p4android/client.py | 4 ++-- bleak/backends/scanner.py | 13 ++----------- bleak/backends/winrt/client.py | 3 +-- 7 files changed, 19 insertions(+), 39 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 3ab419de8..93259dc71 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -23,7 +23,6 @@ Iterable, Literal, Optional, - Set, Type, TypedDict, Union, @@ -75,7 +74,7 @@ # prevent tasks from being garbage collected -_background_tasks: Set[asyncio.Task] = set() +_background_tasks = set[asyncio.Task]() class BleakScanner: diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 80e25d50b..8de9267b2 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -8,7 +8,7 @@ import sys import warnings from contextlib import AsyncExitStack -from typing import Callable, Optional, Set, Union, cast +from typing import Callable, Optional, Union, cast from uuid import UUID if sys.version_info < (3, 12): @@ -47,7 +47,7 @@ logger = logging.getLogger(__name__) # prevent tasks from being garbage collected -_background_tasks: Set[asyncio.Task] = set() +_background_tasks = set[asyncio.Task]() class BleakClientBlueZDBus(BaseBleakClient): @@ -70,7 +70,7 @@ class BleakClientBlueZDBus(BaseBleakClient): def __init__( self, address_or_ble_device: Union[BLEDevice, str], - services: Optional[Set[str]] = None, + services: Optional[set[str]] = None, **kwargs, ): super(BleakClientBlueZDBus, self).__init__(address_or_ble_device, **kwargs) diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index 97dc452db..82cedba4f 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -11,16 +11,7 @@ import logging import os from collections import defaultdict -from typing import ( - Any, - Callable, - Coroutine, - MutableMapping, - NamedTuple, - Optional, - Set, - cast, -) +from typing import Any, Callable, Coroutine, MutableMapping, NamedTuple, Optional, cast from weakref import WeakKeyDictionary from dbus_fast import BusType, Message, MessageType, Variant, unpack_variants @@ -164,24 +155,24 @@ def __init__(self): 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]] = ( defaultdict(list) ) self._device_removed_callbacks: list[DeviceRemovedCallbackAndState] = [] - self._device_watchers: dict[str, Set[DeviceWatcher]] = {} - self._condition_callbacks: dict[str, Set[DeviceConditionCallback]] = {} + 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: @@ -623,7 +614,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. diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 14b64a771..bc72549cd 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -8,7 +8,7 @@ import logging import sys import uuid -from typing import Optional, Set, TypedDict, Union +from typing import Optional, TypedDict, Union if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -75,7 +75,7 @@ class BleakClientCoreBluetooth(BaseBleakClient): def __init__( self, address_or_ble_device: Union[BLEDevice, str], - services: Optional[Set[str]] = None, + services: Optional[set[str]] = None, **kwargs, ): super(BleakClientCoreBluetooth, self).__init__(address_or_ble_device, **kwargs) diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index b9a46842c..aa710c877 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -6,7 +6,7 @@ import logging import uuid import warnings -from typing import Optional, Set, Union +from typing import Optional, Union from android.broadcast import BroadcastReceiver from jnius import java_method @@ -38,7 +38,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) diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 6a642f88a..0d7cc8e20 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -3,22 +3,13 @@ import inspect import os import platform -from typing import ( - Any, - Callable, - Coroutine, - Hashable, - NamedTuple, - Optional, - Set, - Type, -) +from typing import Any, Callable, Coroutine, Hashable, NamedTuple, Optional, Type from ..exc import BleakError from .device import BLEDevice # prevent tasks from being garbage collected -_background_tasks: Set[asyncio.Task] = set() +_background_tasks = set[asyncio.Task]() class AdvertisementData(NamedTuple): diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index dd6b670e7..c3a82c332 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -17,7 +17,6 @@ Optional, Protocol, Sequence, - Set, TypedDict, TypeVar, Union, @@ -181,7 +180,7 @@ 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, From b8ed92c7a10dc9cfc54e4f680975b230fe78b864 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 16 May 2025 20:30:30 -0500 Subject: [PATCH 048/113] typing: use python 3.9 type[T] instead of typing.Type[T] --- bleak/__init__.py | 11 +++++------ bleak/backends/client.py | 4 ++-- bleak/backends/scanner.py | 4 ++-- typings/CoreBluetooth/__init__.pyi | 6 +++--- typings/Foundation/__init__.pyi | 4 ++-- typings/objc/__init__.py | 4 ++-- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 93259dc71..5c8189601 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -23,7 +23,6 @@ Iterable, Literal, Optional, - Type, TypedDict, Union, overload, @@ -134,7 +133,7 @@ def __init__( *, bluez: BlueZScannerArgs = {}, cb: CBScannerArgs = {}, - backend: Optional[Type[BaseBleakScanner]] = None, + backend: Optional[type[BaseBleakScanner]] = None, **kwargs, ) -> None: PlatformBleakScanner = ( @@ -156,7 +155,7 @@ async def __aenter__(self) -> BleakScanner: async def __aexit__( self, - exc_type: Type[BaseException], + exc_type: type[BaseException], exc_val: BaseException, exc_tb: TracebackType, ) -> None: @@ -221,7 +220,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). @@ -452,7 +451,7 @@ def __init__( timeout: float = 10.0, pair: bool = False, winrt: WinRTClientArgs = {}, - backend: Optional[Type[BaseBleakClient]] = None, + backend: Optional[type[BaseBleakClient]] = None, **kwargs, ) -> None: PlatformBleakClient = ( @@ -511,7 +510,7 @@ async def __aenter__(self) -> BleakClient: async def __aexit__( self, - exc_type: Type[BaseException], + exc_type: type[BaseException], exc_val: BaseException, exc_tb: TracebackType, ) -> None: diff --git a/bleak/backends/client.py b/bleak/backends/client.py index 3bb27dd16..376b6e121 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -10,7 +10,7 @@ import platform import sys import uuid -from typing import Callable, Optional, Type, Union +from typing import Callable, Optional, Union if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -221,7 +221,7 @@ async def stop_notify( 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/scanner.py b/bleak/backends/scanner.py index 0d7cc8e20..4bfce3590 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -3,7 +3,7 @@ import inspect import os import platform -from typing import Any, Callable, Coroutine, Hashable, NamedTuple, Optional, Type +from typing import Any, Callable, Coroutine, Hashable, NamedTuple, Optional from ..exc import BleakError from .device import BLEDevice @@ -282,7 +282,7 @@ def set_scanning_filter(self, **kwargs) -> None: 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/typings/CoreBluetooth/__init__.pyi b/typings/CoreBluetooth/__init__.pyi index ae988feb6..92cd8850c 100644 --- a/typings/CoreBluetooth/__init__.pyi +++ b/typings/CoreBluetooth/__init__.pyi @@ -19,16 +19,16 @@ TCBCentralManager = TypeVar("TCBCentralManager", bound=CBCentralManager) class CBCentralManager(CBManager): @classmethod - def init(cls: Type[TCBCentralManager]) -> Optional[TCBCentralManager]: ... + def init(cls: type[TCBCentralManager]) -> Optional[TCBCentralManager]: ... @classmethod def initWithDelegate_queue_( - cls: Type[TCBCentralManager], + cls: type[TCBCentralManager], delegate: CBCentralManagerDelegate, queue: dispatch_queue_t, ) -> Optional[TCBCentralManager]: ... @classmethod def initWithDelegate_queue_options_( - cls: Type[TCBCentralManager], + cls: type[TCBCentralManager], delegate: CBCentralManagerDelegate, queue: dispatch_queue_t, options: NSDictionary, diff --git a/typings/Foundation/__init__.pyi b/typings/Foundation/__init__.pyi index 21e4be06f..86d02a877 100644 --- a/typings/Foundation/__init__.pyi +++ b/typings/Foundation/__init__.pyi @@ -1,10 +1,10 @@ -from typing import NewType, Optional, Sequence, Type, TypeVar +from typing import NewType, Optional, Sequence, TypeVar TNSObject = TypeVar("TNSObject", bound=NSObject) class NSObject: @classmethod - def alloc(cls: Type[TNSObject]) -> TNSObject: ... + def alloc(cls: type[TNSObject]) -> TNSObject: ... def init(self: TNSObject) -> Optional[TNSObject]: ... def addObserver_forKeyPath_options_context_( self, diff --git a/typings/objc/__init__.py b/typings/objc/__init__.py index 0e92f6794..48f091fbe 100644 --- a/typings/objc/__init__.py +++ b/typings/objc/__init__.py @@ -1,11 +1,11 @@ -from typing import Optional, Type, TypeVar +from typing import Optional, TypeVar from Foundation import NSObject T = TypeVar("T") -def super(cls: Type[T], self: T) -> T: ... +def super(cls: type[T], self: T) -> T: ... def macos_available(major: int, minor: int, patch: int = 0) -> bool: ... From bfa00e0776246f6953c888da2e7116b7cd8b716f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 16 May 2025 20:42:13 -0500 Subject: [PATCH 049/113] typing: use collections.abc instead of deprecated typing aliases --- bleak/__init__.py | 14 ++------------ bleak/backends/bluezdbus/advertisement_monitor.py | 3 ++- bleak/backends/bluezdbus/characteristic.py | 3 ++- bleak/backends/bluezdbus/client.py | 3 ++- bleak/backends/bluezdbus/manager.py | 3 ++- bleak/backends/bluezdbus/scanner.py | 3 ++- bleak/backends/bluezdbus/signals.py | 3 ++- bleak/backends/characteristic.py | 3 ++- bleak/backends/client.py | 3 ++- .../corebluetooth/CentralManagerDelegate.py | 3 ++- bleak/backends/corebluetooth/PeripheralDelegate.py | 3 ++- bleak/backends/corebluetooth/characteristic.py | 3 ++- bleak/backends/p4android/characteristic.py | 3 ++- bleak/backends/scanner.py | 3 ++- bleak/backends/service.py | 3 ++- bleak/backends/winrt/characteristic.py | 3 ++- typings/Foundation/__init__.pyi | 3 ++- 17 files changed, 34 insertions(+), 28 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 5c8189601..bd24c3b2a 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -14,19 +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, - Iterable, - Literal, - Optional, - TypedDict, - Union, - overload, -) +from typing import TYPE_CHECKING, Literal, Optional, TypedDict, Union, overload if sys.version_info < (3, 12): from typing_extensions import Buffer diff --git a/bleak/backends/bluezdbus/advertisement_monitor.py b/bleak/backends/bluezdbus/advertisement_monitor.py index 85801efb8..49892035d 100644 --- a/bleak/backends/bluezdbus/advertisement_monitor.py +++ b/bleak/backends/bluezdbus/advertisement_monitor.py @@ -7,7 +7,8 @@ """ import logging -from typing import Iterable, NamedTuple, Union, no_type_check +from collections.abc import Iterable +from typing import NamedTuple, Union, no_type_check from dbus_fast.service import PropertyAccess, ServiceInterface, dbus_property, method diff --git a/bleak/backends/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py index 2e8d406a6..cfc9053b3 100644 --- a/bleak/backends/bluezdbus/characteristic.py +++ b/bleak/backends/bluezdbus/characteristic.py @@ -1,4 +1,5 @@ -from typing import Callable, Union +from collections.abc import Callable +from typing import Union from uuid import UUID from ..characteristic import BleakGATTCharacteristic diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 8de9267b2..3a3439d77 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -7,8 +7,9 @@ import os import sys import warnings +from collections.abc import Callable from contextlib import AsyncExitStack -from typing import Callable, Optional, Union, cast +from typing import Optional, Union, cast from uuid import UUID if sys.version_info < (3, 12): diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index 82cedba4f..42362c43e 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -11,7 +11,8 @@ import logging import os from collections import defaultdict -from typing import Any, Callable, Coroutine, MutableMapping, NamedTuple, Optional, 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 diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 5fd3ffffe..c8e6e775a 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -1,5 +1,6 @@ import logging -from typing import Callable, Coroutine, Literal, Optional, TypedDict +from collections.abc import Callable, Coroutine +from typing import Literal, Optional, TypedDict from dbus_fast import Variant diff --git a/bleak/backends/bluezdbus/signals.py b/bleak/backends/bluezdbus/signals.py index 71d44ca59..e26697ed9 100644 --- a/bleak/backends/bluezdbus/signals.py +++ b/bleak/backends/bluezdbus/signals.py @@ -2,7 +2,8 @@ from __future__ import annotations import re -from typing import Any, Coroutine, Optional +from collections.abc import Coroutine +from typing import Any, Optional from dbus_fast.aio.message_bus import MessageBus from dbus_fast.errors import InvalidObjectPathError diff --git a/bleak/backends/characteristic.py b/bleak/backends/characteristic.py index f551acba2..4266cba30 100644 --- a/bleak/backends/characteristic.py +++ b/bleak/backends/characteristic.py @@ -7,7 +7,8 @@ """ import abc import enum -from typing import Any, Callable, Union +from collections.abc import Callable +from typing import Any, Union from uuid import UUID from ..uuids import uuidstr_to_str diff --git a/bleak/backends/client.py b/bleak/backends/client.py index 376b6e121..c8f49e4e6 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -10,7 +10,8 @@ import platform import sys import uuid -from typing import Callable, Optional, Union +from collections.abc import Callable +from typing import Optional, Union if sys.version_info < (3, 12): from typing_extensions import Buffer diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index ccdec979b..51da681eb 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -10,7 +10,8 @@ import logging import sys import threading -from typing import Any, Callable, 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 diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index 331409bed..7962e0e2b 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -12,7 +12,8 @@ import itertools import logging import sys -from typing import Any, Callable, Iterable, NewType, Optional +from collections.abc import Callable, Iterable +from typing import Any, NewType, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout diff --git a/bleak/backends/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py index 23fb2f38f..f488bacb7 100644 --- a/bleak/backends/corebluetooth/characteristic.py +++ b/bleak/backends/corebluetooth/characteristic.py @@ -5,8 +5,9 @@ """ +from collections.abc import Callable from enum import Enum -from typing import Callable, Optional, Union +from typing import Optional, Union from CoreBluetooth import CBCharacteristic diff --git a/bleak/backends/p4android/characteristic.py b/bleak/backends/p4android/characteristic.py index f0d7aa26f..371dc3bd4 100644 --- a/bleak/backends/p4android/characteristic.py +++ b/bleak/backends/p4android/characteristic.py @@ -1,4 +1,5 @@ -from typing import Callable, Union +from collections.abc import Callable +from typing import Union from uuid import UUID from ...exc import BleakError diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 4bfce3590..30f1bacc7 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -3,7 +3,8 @@ import inspect import os import platform -from typing import Any, Callable, Coroutine, Hashable, NamedTuple, Optional +from collections.abc import Callable, Coroutine, Hashable +from typing import Any, NamedTuple, Optional from ..exc import BleakError from .device import BLEDevice diff --git a/bleak/backends/service.py b/bleak/backends/service.py index f1c2d4f5d..85b9e0cfc 100644 --- a/bleak/backends/service.py +++ b/bleak/backends/service.py @@ -7,7 +7,8 @@ """ import abc import logging -from typing import Any, Iterator, Optional, Union +from collections.abc import Iterator +from typing import Any, Optional, Union from uuid import UUID from ..exc import BleakError diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py index 9e6b09e8c..c977d62ae 100644 --- a/bleak/backends/winrt/characteristic.py +++ b/bleak/backends/winrt/characteristic.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Callable, Union +from collections.abc import Callable +from typing import Union from uuid import UUID from winrt.windows.devices.bluetooth.genericattributeprofile import ( diff --git a/typings/Foundation/__init__.pyi b/typings/Foundation/__init__.pyi index 86d02a877..d53c769df 100644 --- a/typings/Foundation/__init__.pyi +++ b/typings/Foundation/__init__.pyi @@ -1,4 +1,5 @@ -from typing import NewType, Optional, Sequence, TypeVar +from collections.abc import Sequence +from typing import NewType, Optional, TypeVar TNSObject = TypeVar("TNSObject", bound=NSObject) From 8be45401925325e93230f2553e1b99a3c35c7746 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 16 May 2025 21:05:45 -0500 Subject: [PATCH 050/113] CHANGELOG: add link to fixed issue Fixes: https://github.com/hbldh/bleak/issues/1757 --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 06cc09071..c33a448f8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,7 +27,7 @@ Fixed ----- * 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. +* 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, ...) * Fixed ATT error code 15 description to "Insufficient Encryption". Merged #1746. From b9820656d2d28c2e82c47b6ff34308d831f6f821 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 16 May 2025 22:09:22 -0500 Subject: [PATCH 051/113] typing: fix missing generic type arguments --- bleak/__init__.py | 4 ++-- bleak/backends/bluezdbus/client.py | 4 ++-- bleak/backends/bluezdbus/manager.py | 4 ++-- bleak/backends/bluezdbus/scanner.py | 4 ++-- .../corebluetooth/CentralManagerDelegate.py | 4 ++-- .../corebluetooth/PeripheralDelegate.py | 22 +++++++++++-------- bleak/backends/scanner.py | 2 +- bleak/backends/winrt/client.py | 2 +- examples/async_callback_with_queue.py | 11 ++++++---- examples/mtu_size.py | 2 +- typings/Foundation/__init__.pyi | 4 ++-- 11 files changed, 35 insertions(+), 28 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index bd24c3b2a..5e223072e 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -63,7 +63,7 @@ # prevent tasks from being garbage collected -_background_tasks = set[asyncio.Task]() +_background_tasks = set[asyncio.Task[None]]() class BleakScanner: @@ -173,7 +173,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)) diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 3a3439d77..21ddcd4ba 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -48,7 +48,7 @@ logger = logging.getLogger(__name__) # prevent tasks from being garbage collected -_background_tasks = set[asyncio.Task]() +_background_tasks = set[asyncio.Task[None]]() class BleakClientBlueZDBus(BaseBleakClient): @@ -91,7 +91,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 diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index 42362c43e..7b2a103e8 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -356,7 +356,7 @@ async def active_scan( 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. @@ -467,7 +467,7 @@ async def passive_scan( 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. diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index c8e6e775a..a3428d4f8 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -1,6 +1,6 @@ import logging from collections.abc import Callable, Coroutine -from typing import Literal, Optional, TypedDict +from typing import Any, Literal, Optional, TypedDict from dbus_fast import Variant @@ -128,7 +128,7 @@ 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 diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 51da681eb..07aea362d 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -65,13 +65,13 @@ 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._disconnect_callbacks: dict[NSUUID, DisconnectCallback] = {} - self._disconnect_futures: dict[NSUUID, asyncio.Future] = {} + self._disconnect_futures: dict[NSUUID, asyncio.Future[None]] = {} self._did_update_state_event = threading.Event() self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_( diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index 7962e0e2b..b039ef8f0 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -64,27 +64,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] + ] = {} + 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_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[NSNumber]] = {} return self @objc.python_method - def futures(self) -> Iterable[asyncio.Future]: + def futures(self) -> Iterable[asyncio.Future[Any]]: """ Gets all futures for this delegate. diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 30f1bacc7..79d08390c 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -10,7 +10,7 @@ from .device import BLEDevice # prevent tasks from being garbage collected -_background_tasks = set[asyncio.Task]() +_background_tasks = set[asyncio.Task[None]]() class AdvertisementData(NamedTuple): diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index c3a82c332..043eca2c9 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -1058,7 +1058,7 @@ def call_callbacks(): for c in self._callbacks: c(self) - def call_callbacks_threadsafe(op: IAsyncOperation, status: AsyncStatus): + def call_callbacks_threadsafe(op: IAsyncOperation[T], status: AsyncStatus): if status == AsyncStatus.COMPLETED: # have to get result on this thread, otherwise it may not return correct value self._result = op.get_results() diff --git a/examples/async_callback_with_queue.py b/examples/async_callback_with_queue.py index 262854b5d..955fbbd14 100644 --- a/examples/async_callback_with_queue.py +++ b/examples/async_callback_with_queue.py @@ -14,6 +14,7 @@ import asyncio import logging import time +from typing import Optional from bleak import BleakClient, BleakScanner @@ -24,7 +25,9 @@ class DeviceNotFoundError(Exception): pass -async def run_ble_client(args: argparse.Namespace, queue: asyncio.Queue): +async def run_ble_client( + args: argparse.Namespace, queue: asyncio.Queue[tuple[float, Optional[bytearray]]] +): logger.info("starting scan...") if args.address: @@ -44,7 +47,7 @@ async def run_ble_client(args: argparse.Namespace, queue: asyncio.Queue): logger.info("connecting to device...") - async def callback_handler(_, data): + async def callback_handler(_, data: bytearray) -> None: await queue.put((time.time(), data)) async with BleakClient(device) as client: @@ -58,7 +61,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: @@ -74,7 +77,7 @@ async def run_queue_consumer(queue: asyncio.Queue): async def main(args: argparse.Namespace): - queue = asyncio.Queue() + queue = asyncio.Queue[tuple[float, Optional[bytearray]]]() client_task = run_ble_client(args, queue) consumer_task = run_queue_consumer(queue) diff --git a/examples/mtu_size.py b/examples/mtu_size.py index 354163300..426facf34 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 diff --git a/typings/Foundation/__init__.pyi b/typings/Foundation/__init__.pyi index d53c769df..7996e18d9 100644 --- a/typings/Foundation/__init__.pyi +++ b/typings/Foundation/__init__.pyi @@ -1,5 +1,5 @@ from collections.abc import Sequence -from typing import NewType, Optional, TypeVar +from typing import Any, NewType, Optional, TypeVar TNSObject = TypeVar("TNSObject", bound=NSObject) @@ -25,7 +25,7 @@ class NSError(NSObject): ... class NSData(NSObject): ... class NSArray(NSObject): - def initWithArray_(self, array: Sequence) -> NSArray: ... + def initWithArray_(self, array: Sequence[Any]) -> NSArray: ... class NSValue(NSObject): ... class NSNumber(NSValue): ... From 7da9c0ec42bf476ff37b7475eec3b83e8fb44a8c Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 16 May 2025 22:21:43 -0500 Subject: [PATCH 052/113] typing: add explicit type for kwargs --- bleak/__init__.py | 34 +++++++++++++++++++---------- bleak/backends/bluezdbus/client.py | 16 +++++++------- bleak/backends/bluezdbus/scanner.py | 4 ++-- bleak/backends/bluezdbus/signals.py | 2 +- bleak/backends/client.py | 18 +++++++-------- bleak/backends/scanner.py | 2 +- bleak/backends/winrt/client.py | 16 ++++++++------ 7 files changed, 53 insertions(+), 39 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 5e223072e..7eee4880f 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -16,7 +16,7 @@ import uuid from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable from types import TracebackType -from typing import TYPE_CHECKING, Literal, Optional, TypedDict, Union, overload +from typing import TYPE_CHECKING, Any, Literal, Optional, TypedDict, Union, overload if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -124,7 +124,7 @@ def __init__( bluez: BlueZScannerArgs = {}, cb: CBScannerArgs = {}, backend: Optional[type[BaseBleakScanner]] = None, - **kwargs, + **kwargs: Any, ) -> None: PlatformBleakScanner = ( get_platform_scanner_backend_type() if backend is None else backend @@ -219,18 +219,30 @@ class ExtraArgs(TypedDict, total=False): @overload @classmethod async def discover( - cls, timeout: float = 5.0, *, return_adv: Literal[False] = False, **kwargs + 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 + 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. @@ -442,7 +454,7 @@ def __init__( pair: bool = False, winrt: WinRTClientArgs = {}, backend: Optional[type[BaseBleakClient]] = None, - **kwargs, + **kwargs: Any, ) -> None: PlatformBleakClient = ( get_platform_client_backend_type() if backend is None else backend @@ -508,7 +520,7 @@ async def __aexit__( # Connectivity methods - async def connect(self, **kwargs) -> None: + async def connect(self, **kwargs: Any) -> None: """Connect to the specified GATT server. Args: @@ -527,7 +539,7 @@ async def disconnect(self) -> None: """ await self._backend.disconnect() - async def pair(self, *args, **kwargs) -> None: + async def pair(self, *args: Any, **kwargs: Any) -> None: """ Pair with the specified GATT server. @@ -588,7 +600,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. @@ -680,7 +692,7 @@ async def start_notify( ], *, cb: CBStartNotifyArgs = {}, - **kwargs, + **kwargs: Any, ) -> None: """ Activate notifications/indications on a characteristic. @@ -756,7 +768,7 @@ async def stop_notify( """ await self._backend.stop_notify(char_specifier) - async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: + async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: """ Perform read operation on the specified GATT descriptor. diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 21ddcd4ba..d0eb097ce 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -9,7 +9,7 @@ import warnings from collections.abc import Callable from contextlib import AsyncExitStack -from typing import Optional, Union, cast +from typing import Any, Optional, Union, cast from uuid import UUID if sys.version_info < (3, 12): @@ -72,7 +72,7 @@ def __init__( self, address_or_ble_device: Union[BLEDevice, str], services: Optional[set[str]] = None, - **kwargs, + **kwargs: Any, ): super(BleakClientBlueZDBus, self).__init__(address_or_ble_device, **kwargs) # kwarg "device" is for backwards compatibility @@ -107,7 +107,7 @@ def __init__( # Connectivity methods async def connect( - self, pair: bool, dangerous_use_bleak_cache: bool = False, **kwargs + self, pair: bool, dangerous_use_bleak_cache: bool = False, **kwargs: Any ) -> None: """Connect to the specified GATT server. @@ -451,7 +451,7 @@ async def disconnect(self) -> None: # "PropertiesChanged" signal handler and that it completed successfully assert self._bus is None - async def pair(self, *args, **kwargs) -> None: + 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. @@ -637,7 +637,7 @@ def mtu_size(self) -> int: # GATT services methods async def get_services( - self, dangerous_use_bleak_cache: bool = False, **kwargs + self, dangerous_use_bleak_cache: bool = False, **kwargs: Any ) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. @@ -667,7 +667,7 @@ async def get_services( async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID], - **kwargs, + **kwargs: Any, ) -> bytearray: """Perform read operation on the specified GATT characteristic. @@ -737,7 +737,7 @@ async def read_gatt_char( ) return value - async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: + async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: @@ -878,7 +878,7 @@ async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, - **kwargs, + **kwargs: Any, ) -> None: """ Activate notifications/indications on a characteristic. diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index a3428d4f8..2f5a3344c 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -118,7 +118,7 @@ def __init__( scanning_mode: Literal["active", "passive"], *, bluez: BlueZScannerArgs, - **kwargs, + **kwargs: Any, ): super(BleakScannerBlueZDBus, self).__init__(detection_callback, service_uuids) @@ -187,7 +187,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 diff --git a/bleak/backends/bluezdbus/signals.py b/bleak/backends/bluezdbus/signals.py index e26697ed9..fe18ffbc6 100644 --- a/bleak/backends/bluezdbus/signals.py +++ b/bleak/backends/bluezdbus/signals.py @@ -70,7 +70,7 @@ 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 diff --git a/bleak/backends/client.py b/bleak/backends/client.py index c8f49e4e6..ba773e413 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -11,7 +11,7 @@ import sys import uuid from collections.abc import Callable -from typing import Optional, Union +from typing import Any, Optional, Union if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -41,7 +41,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: @@ -63,7 +63,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. @@ -77,7 +77,7 @@ def set_disconnected_callback( self._disconnected_callback = callback @abc.abstractmethod - async def connect(self, pair: bool, **kwargs) -> None: + async def connect(self, pair: bool, **kwargs: Any) -> None: """Connect to the specified GATT server. Args: @@ -95,7 +95,7 @@ async def disconnect(self) -> None: raise NotImplementedError() @abc.abstractmethod - async def pair(self, *args, **kwargs) -> None: + async def pair(self, *args: Any, **kwargs: Any) -> None: """Pair with the peripheral.""" raise NotImplementedError() @@ -118,7 +118,7 @@ def is_connected(self) -> bool: # GATT services methods @abc.abstractmethod - async def get_services(self, **kwargs) -> BleakGATTServiceCollection: + async def get_services(self, **kwargs: Any) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. Returns: @@ -133,7 +133,7 @@ async def get_services(self, **kwargs) -> 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. @@ -149,7 +149,7 @@ 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, handle: int, **kwargs: Any) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: @@ -194,7 +194,7 @@ async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, - **kwargs, + **kwargs: Any, ) -> None: """ Activate notifications/indications on a characteristic. diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 79d08390c..7c3376dc1 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -273,7 +273,7 @@ async def stop(self) -> None: raise NotImplementedError() @abc.abstractmethod - def set_scanning_filter(self, **kwargs) -> None: + def set_scanning_filter(self, **kwargs: Any) -> None: """Set scanning filter for the BleakScanner. Args: diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 043eca2c9..571728fc8 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -183,7 +183,7 @@ def __init__( services: Optional[set[str]] = None, *, winrt: WinRTClientArgs, - **kwargs, + **kwargs: Any, ): super(BleakClientWinRT, self).__init__(address_or_ble_device, **kwargs) @@ -242,7 +242,7 @@ async def _create_requester(self, bluetooth_address: int) -> BluetoothLEDevice: ) return requester - async def connect(self, pair: bool, **kwargs) -> None: + async def connect(self, pair: bool, **kwargs: Any) -> None: """Connect to the specified GATT server. Keyword Args: @@ -548,7 +548,9 @@ def mtu_size(self) -> int: return self._session.max_pdu_size async def pair( - self, protection_level: Optional[DevicePairingProtectionLevel] = None, **kwargs + self, + protection_level: Optional[DevicePairingProtectionLevel] = None, + **kwargs: Any, ) -> None: """Attempts to pair with the device. @@ -641,7 +643,7 @@ async def get_services( *, service_cache_mode: Optional[BluetoothCacheMode] = None, cache_mode: Optional[BluetoothCacheMode] = None, - **kwargs, + **kwargs: Any, ) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. @@ -782,7 +784,7 @@ async def get_services( 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. @@ -831,7 +833,7 @@ async def read_gatt_char( return value - async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: + async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: @@ -941,7 +943,7 @@ async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, - **kwargs, + **kwargs: Any, ) -> None: """ Activate notifications/indications on a characteristic. From 6f042b11cec2f70df1fa5ee75eeeb8377142a53d Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 16 May 2025 22:32:06 -0500 Subject: [PATCH 053/113] typing: fix unreachable --- bleak/__init__.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 7eee4880f..ce88990e3 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -16,7 +16,16 @@ import uuid from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable from types import TracebackType -from typing import TYPE_CHECKING, Any, Literal, Optional, TypedDict, Union, overload +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Optional, + TypedDict, + Union, + cast, + overload, +) if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -25,10 +34,10 @@ if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout - from typing_extensions import Unpack + from typing_extensions import Never, Unpack, assert_never else: from asyncio import timeout as async_timeout - from typing import Unpack + from typing import Never, Unpack, assert_never from .backends.characteristic import BleakGATTCharacteristic from .backends.client import BaseBleakClient, get_platform_client_backend_type @@ -375,6 +384,7 @@ 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 From 578421eef0d98ca28124c76bf6f33516c99634f0 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 18:29:22 -0500 Subject: [PATCH 054/113] backends: bluez: fix wrong import module Fixes: "PropertyAccess" is not exported from module "dbus_fast.service" Import from "dbus_fast.constants" insteadPylancereportPrivateImportUsage --- bleak/backends/bluezdbus/advertisement_monitor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bleak/backends/bluezdbus/advertisement_monitor.py b/bleak/backends/bluezdbus/advertisement_monitor.py index 49892035d..ff937a068 100644 --- a/bleak/backends/bluezdbus/advertisement_monitor.py +++ b/bleak/backends/bluezdbus/advertisement_monitor.py @@ -10,7 +10,8 @@ from collections.abc import Iterable from typing import NamedTuple, Union, no_type_check -from dbus_fast.service import PropertyAccess, ServiceInterface, dbus_property, method +from dbus_fast import PropertyAccess +from dbus_fast.service import ServiceInterface, dbus_property, method from ...assigned_numbers import AdvertisementDataType from . import defs From 3321c30dc78f2d46ecb22ed3b520986008190b16 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 18:57:40 -0500 Subject: [PATCH 055/113] backends: add hack to ignore different platforms from type checkers This makes a bunch of error squiggles due to platform-specific dependencies not being installed go away in the IDE. --- .vscode/settings.json | 3 ++- bleak/backends/bluezdbus/advertisement_monitor.py | 7 +++++++ bleak/backends/bluezdbus/characteristic.py | 7 +++++++ bleak/backends/bluezdbus/client.py | 8 +++++++- bleak/backends/bluezdbus/descriptor.py | 7 +++++++ bleak/backends/bluezdbus/manager.py | 7 +++++++ bleak/backends/bluezdbus/scanner.py | 7 +++++++ bleak/backends/bluezdbus/service.py | 7 +++++++ bleak/backends/bluezdbus/signals.py | 8 +++++++- bleak/backends/bluezdbus/utils.py | 8 +++++++- bleak/backends/bluezdbus/version.py | 7 +++++++ bleak/backends/corebluetooth/CentralManagerDelegate.py | 8 +++++++- bleak/backends/corebluetooth/PeripheralDelegate.py | 8 +++++++- bleak/backends/corebluetooth/__init__.py | 6 ++++++ bleak/backends/corebluetooth/characteristic.py | 7 +++++++ bleak/backends/corebluetooth/client.py | 8 +++++++- bleak/backends/corebluetooth/descriptor.py | 7 +++++++ bleak/backends/corebluetooth/scanner.py | 7 +++++++ bleak/backends/corebluetooth/service.py | 7 +++++++ bleak/backends/corebluetooth/utils.py | 7 +++++++ bleak/backends/p4android/characteristic.py | 7 +++++++ bleak/backends/p4android/client.py | 7 +++++++ bleak/backends/p4android/defs.py | 7 ++++++- bleak/backends/p4android/descriptor.py | 7 +++++++ bleak/backends/p4android/scanner.py | 8 ++++++-- bleak/backends/p4android/service.py | 7 +++++++ bleak/backends/p4android/utils.py | 7 ++++++- bleak/backends/winrt/characteristic.py | 8 +++++++- bleak/backends/winrt/client.py | 7 ++++++- bleak/backends/winrt/descriptor.py | 7 ++++++- bleak/backends/winrt/scanner.py | 7 +++++++ bleak/backends/winrt/service.py | 7 +++++++ bleak/backends/winrt/util.py | 8 +++++++- pyproject.toml | 4 ++++ 34 files changed, 224 insertions(+), 15 deletions(-) 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/bleak/backends/bluezdbus/advertisement_monitor.py b/bleak/backends/bluezdbus/advertisement_monitor.py index ff937a068..810acae2c 100644 --- a/bleak/backends/bluezdbus/advertisement_monitor.py +++ b/bleak/backends/bluezdbus/advertisement_monitor.py @@ -6,6 +6,13 @@ monitor api `. """ +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 collections.abc import Iterable from typing import NamedTuple, Union, no_type_check diff --git a/bleak/backends/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py index cfc9053b3..02885e6da 100644 --- a/bleak/backends/bluezdbus/characteristic.py +++ b/bleak/backends/bluezdbus/characteristic.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" + from collections.abc import Callable from typing import Union from uuid import UUID diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index d0eb097ce..c99dfd3d1 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -2,10 +2,16 @@ """ 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 collections.abc import Callable from contextlib import AsyncExitStack diff --git a/bleak/backends/bluezdbus/descriptor.py b/bleak/backends/bluezdbus/descriptor.py index bf3507990..25dc7368d 100644 --- a/bleak/backends/bluezdbus/descriptor.py +++ b/bleak/backends/bluezdbus/descriptor.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" + from ..descriptor import BleakGATTDescriptor from .defs import GattDescriptor1 diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index 7b2a103e8..8de3d8cf2 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -6,6 +6,13 @@ 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 diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 2f5a3344c..d5c633951 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.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 logging from collections.abc import Callable, Coroutine from typing import Any, Literal, Optional, TypedDict diff --git a/bleak/backends/bluezdbus/service.py b/bleak/backends/bluezdbus/service.py index 1febdcc2c..04c62e103 100644 --- a/bleak/backends/bluezdbus/service.py +++ b/bleak/backends/bluezdbus/service.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" + from typing import Any from ..service import BleakGATTService diff --git a/bleak/backends/bluezdbus/signals.py b/bleak/backends/bluezdbus/signals.py index fe18ffbc6..cded90a74 100644 --- a/bleak/backends/bluezdbus/signals.py +++ b/bleak/backends/bluezdbus/signals.py @@ -1,8 +1,14 @@ # -*- 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 collections.abc import Coroutine from typing import Any, Optional from dbus_fast.aio.message_bus import MessageBus diff --git a/bleak/backends/bluezdbus/utils.py b/bleak/backends/bluezdbus/utils.py index abf81c4f6..1f37eada8 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 diff --git a/bleak/backends/bluezdbus/version.py b/bleak/backends/bluezdbus/version.py index f15d89299..46042059a 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 diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 07aea362d..56eb432ab 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -6,9 +6,15 @@ """ +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 collections.abc import Callable from typing import Any, Optional diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index b039ef8f0..79cb47bfe 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -8,10 +8,16 @@ 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 collections.abc import Callable, Iterable from typing import Any, NewType, Optional diff --git a/bleak/backends/corebluetooth/__init__.py b/bleak/backends/corebluetooth/__init__.py index ed160a3dd..1bbe4f840 100644 --- a/bleak/backends/corebluetooth/__init__.py +++ b/bleak/backends/corebluetooth/__init__.py @@ -5,6 +5,12 @@ 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 index f488bacb7..ebdf175c7 100644 --- a/bleak/backends/corebluetooth/characteristic.py +++ b/bleak/backends/corebluetooth/characteristic.py @@ -5,6 +5,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 collections.abc import Callable from enum import Enum from typing import Optional, Union diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index bc72549cd..15a74c97e 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -4,9 +4,15 @@ 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, TypedDict, Union diff --git a/bleak/backends/corebluetooth/descriptor.py b/bleak/backends/corebluetooth/descriptor.py index 646a16057..c87ff004c 100644 --- a/bleak/backends/corebluetooth/descriptor.py +++ b/bleak/backends/corebluetooth/descriptor.py @@ -5,6 +5,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 CBDescriptor from ..corebluetooth.utils import cb_uuid_to_str diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index 6daf595b0..403bbd50e 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -1,3 +1,10 @@ +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, Literal, Optional, TypedDict diff --git a/bleak/backends/corebluetooth/service.py b/bleak/backends/corebluetooth/service.py index 00255e153..2632ec321 100644 --- a/bleak/backends/corebluetooth/service.py +++ b/bleak/backends/corebluetooth/service.py @@ -1,3 +1,10 @@ +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 CBService from ..service import BleakGATTService diff --git a/bleak/backends/corebluetooth/utils.py b/bleak/backends/corebluetooth/utils.py index 920954739..01f4f6ed0 100644 --- a/bleak/backends/corebluetooth/utils.py +++ b/bleak/backends/corebluetooth/utils.py @@ -1,3 +1,10 @@ +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 diff --git a/bleak/backends/p4android/characteristic.py b/bleak/backends/p4android/characteristic.py index 371dc3bd4..06cce6117 100644 --- a/bleak/backends/p4android/characteristic.py +++ b/bleak/backends/p4android/characteristic.py @@ -1,3 +1,10 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "android": + assert False, "This backend is only available on Android" + from collections.abc import Callable from typing import Union from uuid import UUID diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index aa710c877..3e1fa449d 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -2,6 +2,13 @@ """ 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 diff --git a/bleak/backends/p4android/defs.py b/bleak/backends/p4android/defs.py index 832f92bc3..50cc47fbd 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 diff --git a/bleak/backends/p4android/descriptor.py b/bleak/backends/p4android/descriptor.py index 844316ec8..5a30b39ee 100644 --- a/bleak/backends/p4android/descriptor.py +++ b/bleak/backends/p4android/descriptor.py @@ -1,3 +1,10 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "android": + assert False, "This backend is only available on Android" + from ..descriptor import BleakGATTDescriptor diff --git a/bleak/backends/p4android/scanner.py b/bleak/backends/p4android/scanner.py index ef57cdae7..7689f0106 100644 --- a/bleak/backends/p4android/scanner.py +++ b/bleak/backends/p4android/scanner.py @@ -1,8 +1,12 @@ -# -*- 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 Literal, Optional diff --git a/bleak/backends/p4android/service.py b/bleak/backends/p4android/service.py index 2d49d5ca3..363dd6e01 100644 --- a/bleak/backends/p4android/service.py +++ b/bleak/backends/p4android/service.py @@ -1,3 +1,10 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "android": + assert False, "This backend is only available on Android" + from ..service import BleakGATTService from .characteristic import BleakGATTCharacteristicP4Android diff --git a/bleak/backends/p4android/utils.py b/bleak/backends/p4android/utils.py index a4fafda38..a2ec0610f 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 diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py index c977d62ae..53fcb4bce 100644 --- a/bleak/backends/winrt/characteristic.py +++ b/bleak/backends/winrt/characteristic.py @@ -1,4 +1,10 @@ -# -*- coding: utf-8 -*- +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "win32": + assert False, "This backend is only available on Windows" + from collections.abc import Callable from typing import Union from uuid import UUID diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 571728fc8..79bee9efa 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -4,10 +4,15 @@ 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 from ctypes import WinError from typing import ( diff --git a/bleak/backends/winrt/descriptor.py b/bleak/backends/winrt/descriptor.py index 8eb493194..02bf76757 100644 --- a/bleak/backends/winrt/descriptor.py +++ b/bleak/backends/winrt/descriptor.py @@ -1,4 +1,9 @@ -# -*- coding: utf-8 -*- +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "win32": + assert False, "This backend is only available on Windows" from winrt.windows.devices.bluetooth.genericattributeprofile import GattDescriptor diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index f63a21658..5270980e1 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -1,3 +1,10 @@ +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 from typing import Literal, NamedTuple, Optional diff --git a/bleak/backends/winrt/service.py b/bleak/backends/winrt/service.py index 9bd4d4fcc..1e6afc60a 100644 --- a/bleak/backends/winrt/service.py +++ b/bleak/backends/winrt/service.py @@ -1,3 +1,10 @@ +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "win32": + assert False, "This backend is only available on Windows" + from winrt.windows.devices.bluetooth.genericattributeprofile import GattDeviceService from ..service import BleakGATTService diff --git a/bleak/backends/winrt/util.py b/bleak/backends/winrt/util.py index 21741ab9a..8802a7885 100644 --- a/bleak/backends/winrt/util.py +++ b/bleak/backends/winrt/util.py @@ -1,6 +1,12 @@ +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 diff --git a/pyproject.toml b/pyproject.toml index f55dab243..9ad11b6be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,3 +68,7 @@ profile = "black" py_version=38 src_paths = ["bleak", "examples", "tests", "typings"] extend_skip = [".buildozer"] + +[tool.mypy] +python_version = "3.9" +disable_error_code = ["import-not-found"] From 003d2dbfb162fc1a7b27614c3d6b06e0575b9549 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 19:00:19 -0500 Subject: [PATCH 056/113] examples: fix wrong kwarg name --- examples/two_devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/two_devices.py b/examples/two_devices.py index 86082912e..8ddfbfadf 100644 --- a/examples/two_devices.py +++ b/examples/two_devices.py @@ -44,7 +44,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=dict(use_bdaddr=macos_use_bdaddr) ) else: device = await BleakScanner.find_device_by_name(name_or_address) From 72b6b2201c917f31dc08bfe6d94947eaf01b1f8e Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 19:39:32 -0500 Subject: [PATCH 057/113] general type hint improvements Fixing type checker errors. --- bleak/__init__.py | 13 +++++++------ bleak/exc.py | 4 ++-- docs/conf.py | 16 ++++++++-------- examples/service_explorer.py | 2 +- examples/two_devices.py | 7 +++---- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index ce88990e3..e48259796 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -28,9 +28,10 @@ ) if sys.version_info < (3, 12): - from typing_extensions import Buffer + from typing_extensions import Buffer, Self else: from collections.abc import Buffer + from typing import Self if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout @@ -148,7 +149,7 @@ def __init__( **kwargs, ) - async def __aenter__(self) -> BleakScanner: + async def __aenter__(self) -> Self: await self._backend.start() return self @@ -516,15 +517,15 @@ 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() diff --git a/bleak/exc.py b/bleak/exc.py index 567054113..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``. diff --git a/docs/conf.py b/docs/conf.py index 642c7df71..0e7b2d06e 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -213,14 +213,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/examples/service_explorer.py b/examples/service_explorer.py index dfa558b99..f28f3a39a 100644 --- a/examples/service_explorer.py +++ b/examples/service_explorer.py @@ -131,7 +131,7 @@ async def main(args: Args): help="sets the log level to debug", ) - args: 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 8ddfbfadf..d5fcab704 100644 --- a/examples/two_devices.py +++ b/examples/two_devices.py @@ -5,6 +5,7 @@ from typing import Iterable from bleak import BleakClient, BleakScanner +from bleak.backends.characteristic import BleakGATTCharacteristic async def connect_to_device( @@ -55,11 +56,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 +70,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) From 7147f6ca7aa71990f531b5e415b014bc2c78aec2 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 19:39:56 -0500 Subject: [PATCH 058/113] backends: bluez: type hint improvements --- bleak/backends/bluezdbus/client.py | 18 +++++++++++++++-- bleak/backends/bluezdbus/manager.py | 26 ++++++++++++++++++++----- bleak/backends/bluezdbus/service.py | 2 +- bleak/backends/bluezdbus/signals.py | 30 +++++++++++++++++------------ bleak/backends/bluezdbus/utils.py | 1 + bleak/backends/bluezdbus/version.py | 3 ++- 6 files changed, 59 insertions(+), 21 deletions(-) diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index c99dfd3d1..999c1a1e1 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -224,6 +224,7 @@ async def disconnect_device() -> None: member="Disconnect", ) ) + assert disconnect_reply try: assert_reply(disconnect_reply) except BleakDBusError as e: @@ -271,6 +272,7 @@ async def disconnect_device() -> None: ], ) ) + assert reply assert_reply(reply) # REVIST: This leaves "Trusted" property set if we @@ -295,8 +297,7 @@ async def disconnect_device() -> None: member="Connect", ) ) - - assert reply is not None + assert reply if reply.message_type == MessageType.ERROR: # This error is often caused by RF interference @@ -447,6 +448,7 @@ async def disconnect(self) -> None: member="Disconnect", ) ) + assert reply assert_reply(reply) async with async_timeout(10): await self._disconnecting_event.wait() @@ -463,6 +465,7 @@ async def pair(self, *args: Any, **kwargs: Any) -> None: 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. """ + assert self._bus # See if it is already paired. reply = await self._bus.call( Message( @@ -474,6 +477,7 @@ async def pair(self, *args: Any, **kwargs: Any) -> None: 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) @@ -490,6 +494,7 @@ async def pair(self, *args: Any, **kwargs: Any) -> None: body=[defs.DEVICE_INTERFACE, "Trusted", Variant("b", True)], ) ) + assert reply assert_reply(reply) logger.debug("Pairing to BLE device @ %s", self.address) @@ -502,6 +507,7 @@ async def pair(self, *args: Any, **kwargs: Any) -> None: member="Pair", ) ) + assert reply assert_reply(reply) async def unpair(self) -> None: @@ -526,6 +532,8 @@ async def unpair(self) -> None: self._device_info = None self._is_connected = False + assert manager._bus + try: reply = await manager._bus.call( Message( @@ -537,6 +545,7 @@ async def unpair(self) -> None: body=[device_path], ) ) + assert reply assert_reply(reply) except BleakDBusError as e: if e.dbus_error == "org.bluez.Error.DoesNotExist": @@ -582,6 +591,8 @@ async def _acquire_mtu(self) -> None: if "notify" in c.properties ) + assert self._bus + reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, @@ -592,6 +603,7 @@ async def _acquire_mtu(self) -> None: body=[{}], ) ) + assert reply assert_reply(reply) # we aren't actually using the write or notify, we just want the MTU @@ -903,6 +915,7 @@ async def start_notify( member="StartNotify", ) ) + assert reply assert_reply(reply) async def stop_notify( @@ -935,6 +948,7 @@ async def stop_notify( member="StopNotify", ) ) + assert reply assert_reply(reply) self._notification_callbacks.pop(characteristic.path, None) diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index 8de3d8cf2..0a4d48b8b 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -155,7 +155,7 @@ 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() @@ -244,6 +244,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, @@ -280,6 +281,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 @@ -382,6 +384,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. @@ -406,6 +410,7 @@ async def active_scan( body=[filters], ) ) + assert reply assert_reply(reply) # Start scanning @@ -417,6 +422,7 @@ async def active_scan( member="StartDiscovery", ) ) + assert reply assert_reply(reply) async def stop() -> None: @@ -431,6 +437,8 @@ async def stop() -> None: ) async with self._bus_lock: + assert self._bus + reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, @@ -439,6 +447,7 @@ async def stop() -> None: member="StopDiscovery", ) ) + assert reply try: assert_reply(reply) @@ -457,6 +466,7 @@ async def stop() -> None: body=[{}], ) ) + assert reply assert_reply(reply) return stop @@ -493,6 +503,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. @@ -522,6 +534,7 @@ async def passive_scan( body=[monitor_path], ) ) + assert reply if ( reply.message_type == MessageType.ERROR @@ -549,6 +562,8 @@ async def stop() -> None: ) async with self._bus_lock: + assert self._bus + self._bus.unexport(monitor_path, monitor) reply = await self._bus.call( @@ -561,6 +576,7 @@ async def stop() -> None: body=[monitor_path], ) ) + assert reply assert_reply(reply) return stop @@ -985,7 +1001,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 @@ -1020,10 +1036,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/service.py b/bleak/backends/bluezdbus/service.py index 04c62e103..c73296ba2 100644 --- a/bleak/backends/bluezdbus/service.py +++ b/bleak/backends/bluezdbus/service.py @@ -17,7 +17,7 @@ class BleakGATTServiceBlueZDBus(BleakGATTService): def __init__(self, obj: Any, path: str): super().__init__(obj) - self.__characteristics = [] + self.__characteristics: list[BleakGATTCharacteristicBlueZDBus] = [] self.__path = path self.__handle = extract_service_handle_from_path(path) diff --git a/bleak/backends/bluezdbus/signals.py b/bleak/backends/bluezdbus/signals.py index cded90a74..2e64eb186 100644 --- a/bleak/backends/bluezdbus/signals.py +++ b/bleak/backends/bluezdbus/signals.py @@ -83,31 +83,31 @@ def __init__( 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 @@ -118,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 @@ -139,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 @@ -181,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", @@ -193,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", @@ -207,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 1f37eada8..17d5dc7fa 100644 --- a/bleak/backends/bluezdbus/utils.py +++ b/bleak/backends/bluezdbus/utils.py @@ -23,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 diff --git a/bleak/backends/bluezdbus/version.py b/bleak/backends/bluezdbus/version.py index 46042059a..fb0edafa5 100644 --- a/bleak/backends/bluezdbus/version.py +++ b/bleak/backends/bluezdbus/version.py @@ -14,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() From e52929dea5d620c11d10f8280baad427343156ab Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 20:22:24 -0500 Subject: [PATCH 059/113] backends: winrt: more typing improvements --- bleak/backends/winrt/characteristic.py | 2 +- bleak/backends/winrt/client.py | 50 +++++++++++++++++--------- bleak/backends/winrt/scanner.py | 37 +++++++++++++------ bleak/backends/winrt/service.py | 6 ++-- bleak/backends/winrt/util.py | 4 +-- 5 files changed, 65 insertions(+), 34 deletions(-) diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py index 53fcb4bce..51c1efb56 100644 --- a/bleak/backends/winrt/characteristic.py +++ b/bleak/backends/winrt/characteristic.py @@ -134,7 +134,7 @@ def get_descriptor( except StopIteration: return None - def add_descriptor(self, descriptor: BleakGATTDescriptor): + 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. diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 79bee9efa..e023714e9 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -5,6 +5,8 @@ Created on 2020-08-19 by hbldh """ import sys +from collections.abc import Callable +from contextvars import Context from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -29,15 +31,18 @@ ) if sys.version_info < (3, 12): - from typing_extensions import Buffer + from typing_extensions import Buffer, Self else: - from collections.abc import Buffer + from collections.abc import Buffer, Self if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout + from typing_extensions import assert_never else: from asyncio import timeout as async_timeout + from typing import assert_never +from winrt.system import Object from winrt.windows.devices.bluetooth import ( BluetoothAddressType, BluetoothCacheMode, @@ -220,7 +225,7 @@ def __init__( 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 @@ -279,14 +284,14 @@ async def connect(self, pair: bool, **kwargs: Any) -> None: if pair: await self.pair(**kwargs) - def handle_services_changed(): + 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) @@ -295,7 +300,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( @@ -328,7 +333,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) @@ -359,7 +364,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: @@ -978,9 +983,9 @@ 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 @@ -1054,18 +1059,20 @@ class FutureLike(Generic[T]): _asyncio_future_blocking = False - def __init__(self, op: IAsyncOperation[T]) -> 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[T], 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() @@ -1096,19 +1103,26 @@ def result(self) -> T: 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 @@ -1138,6 +1152,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/scanner.py b/bleak/backends/winrt/scanner.py index 5270980e1..533d06295 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -1,5 +1,5 @@ import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: if sys.platform != "win32": @@ -15,6 +15,7 @@ BluetoothLEAdvertisementType, BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStatus, + BluetoothLEAdvertisementWatcherStoppedEventArgs, BluetoothLEScanningMode, ) from winrt.windows.foundation import EventRegistrationToken @@ -79,7 +80,7 @@ def __init__( detection_callback: Optional[AdvertisementDataCallback], service_uuids: Optional[list[str]], scanning_mode: Literal["active", "passive"], - **kwargs, + **kwargs: Any, ): super(BleakScannerWinRT, self).__init__(detection_callback, service_uuids) @@ -134,7 +135,7 @@ def _received_handler( self._advertisement_pairs[bdaddr] = raw_data - uuids = [] + uuids: list[str] = [] mfg_data = {} service_data = {} local_name = None @@ -207,12 +208,17 @@ def _received_handler( 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() async def start(self) -> None: @@ -233,12 +239,21 @@ async def start(self) -> None: 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 @@ -284,7 +299,7 @@ async def stop(self) -> None: self.watcher = None - def set_scanning_filter(self, **kwargs) -> None: + def set_scanning_filter(self, **kwargs: Any) -> None: """Set a scanning filter for the BleakScanner. Keyword Args: diff --git a/bleak/backends/winrt/service.py b/bleak/backends/winrt/service.py index 1e6afc60a..4635561af 100644 --- a/bleak/backends/winrt/service.py +++ b/bleak/backends/winrt/service.py @@ -14,9 +14,9 @@ class BleakGATTServiceWinRT(BleakGATTService): """GATT Characteristic implementation for the .NET backend, implemented with WinRT""" - def __init__(self, obj: GattDeviceService): + def __init__(self, obj: GattDeviceService) -> None: super().__init__(obj) - self.__characteristics = [] + self.__characteristics: list[BleakGATTCharacteristicWinRT] = [] @property def uuid(self) -> str: @@ -31,7 +31,7 @@ def characteristics(self) -> list[BleakGATTCharacteristicWinRT]: """List of characteristics for this service""" return self.__characteristics - def add_characteristic(self, characteristic: BleakGATTCharacteristicWinRT): + def add_characteristic(self, characteristic: BleakGATTCharacteristicWinRT) -> None: """Add a :py:class:`~BleakGATTCharacteristicWinRT` to the service. Should not be used by end user, but rather by `bleak` itself. diff --git a/bleak/backends/winrt/util.py b/bleak/backends/winrt/util.py index 8802a7885..68546f3e5 100644 --- a/bleak/backends/winrt/util.py +++ b/bleak/backends/winrt/util.py @@ -18,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) From f518dea635944ac0e70f76184a290a1dc19f3f64 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 20:42:27 -0500 Subject: [PATCH 060/113] typings: fix wrong file extension --- typings/objc/{__init__.py => __init__.pyi} | 5 ----- 1 file changed, 5 deletions(-) rename typings/objc/{__init__.py => __init__.pyi} (98%) diff --git a/typings/objc/__init__.py b/typings/objc/__init__.pyi similarity index 98% rename from typings/objc/__init__.py rename to typings/objc/__init__.pyi index 48f091fbe..ccb4348db 100644 --- a/typings/objc/__init__.py +++ b/typings/objc/__init__.pyi @@ -4,14 +4,9 @@ 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]: ... From fa03c5c5a05149305d39ed16da389ba4cf5baec5 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 20:51:06 -0500 Subject: [PATCH 061/113] use absolute import instead of relative --- bleak/__init__.py | 22 ++++++------- .../bluezdbus/advertisement_monitor.py | 4 +-- bleak/backends/bluezdbus/characteristic.py | 8 ++--- bleak/backends/bluezdbus/client.py | 24 +++++++------- bleak/backends/bluezdbus/descriptor.py | 4 +-- bleak/backends/bluezdbus/manager.py | 28 +++++++++++------ bleak/backends/bluezdbus/scanner.py | 16 ++++++---- bleak/backends/bluezdbus/service.py | 6 ++-- bleak/backends/bluezdbus/utils.py | 2 +- bleak/backends/characteristic.py | 4 +-- bleak/backends/client.py | 8 ++--- .../corebluetooth/CentralManagerDelegate.py | 2 +- .../corebluetooth/PeripheralDelegate.py | 4 +-- .../backends/corebluetooth/characteristic.py | 8 ++--- bleak/backends/corebluetooth/client.py | 31 +++++++++++-------- bleak/backends/corebluetooth/descriptor.py | 4 +-- bleak/backends/corebluetooth/scanner.py | 12 ++++--- bleak/backends/corebluetooth/service.py | 8 +++-- bleak/backends/corebluetooth/utils.py | 2 +- bleak/backends/descriptor.py | 2 +- bleak/backends/p4android/characteristic.py | 8 ++--- bleak/backends/p4android/client.py | 18 +++++------ bleak/backends/p4android/descriptor.py | 2 +- bleak/backends/p4android/scanner.py | 10 ++++-- bleak/backends/p4android/service.py | 4 +-- bleak/backends/p4android/utils.py | 2 +- bleak/backends/scanner.py | 4 +-- bleak/backends/service.py | 8 ++--- bleak/backends/winrt/characteristic.py | 4 +-- bleak/backends/winrt/client.py | 20 ++++++------ bleak/backends/winrt/descriptor.py | 2 +- bleak/backends/winrt/scanner.py | 14 ++++++--- bleak/backends/winrt/service.py | 4 +-- bleak/backends/winrt/util.py | 2 +- pyproject.toml | 2 +- typings/CoreBluetooth/__init__.pyi | 6 ++-- 36 files changed, 170 insertions(+), 139 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index e48259796..67b16a1b0 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -40,25 +40,25 @@ from asyncio import timeout as async_timeout from typing import Never, Unpack, assert_never -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 bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type +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 +from bleak.backends.service import BleakGATTServiceCollection +from bleak.exc import BleakCharacteristicNotFoundError, BleakError +from bleak.uuids import normalize_uuid_str if TYPE_CHECKING: - from .backends.bluezdbus.scanner import BlueZScannerArgs - from .backends.corebluetooth.client import CBStartNotifyArgs - from .backends.corebluetooth.scanner import CBScannerArgs - from .backends.winrt.client import WinRTClientArgs + from bleak.backends.bluezdbus.scanner import BlueZScannerArgs + from bleak.backends.corebluetooth.client import CBStartNotifyArgs + from bleak.backends.corebluetooth.scanner import CBScannerArgs + from bleak.backends.winrt.client import WinRTClientArgs _logger = logging.getLogger(__name__) diff --git a/bleak/backends/bluezdbus/advertisement_monitor.py b/bleak/backends/bluezdbus/advertisement_monitor.py index 810acae2c..f6f944d29 100644 --- a/bleak/backends/bluezdbus/advertisement_monitor.py +++ b/bleak/backends/bluezdbus/advertisement_monitor.py @@ -20,8 +20,8 @@ from dbus_fast import PropertyAccess from dbus_fast.service import ServiceInterface, dbus_property, method -from ...assigned_numbers import AdvertisementDataType -from . import defs +from bleak.assigned_numbers import AdvertisementDataType +from bleak.backends.bluezdbus import defs logger = logging.getLogger(__name__) diff --git a/bleak/backends/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py index 02885e6da..12ed5c961 100644 --- a/bleak/backends/bluezdbus/characteristic.py +++ b/bleak/backends/bluezdbus/characteristic.py @@ -9,10 +9,10 @@ from typing import 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 +from bleak.backends.bluezdbus.defs import GattCharacteristic1 +from bleak.backends.bluezdbus.utils import extract_service_handle_from_path +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.descriptor import BleakGATTDescriptor class BleakGATTCharacteristicBlueZDBus(BleakGATTCharacteristic): diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 999c1a1e1..f27823b0d 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -33,23 +33,23 @@ from dbus_fast.message import Message from dbus_fast.signature import Variant -from ... import BleakScanner -from ...exc import ( +from bleak import BleakScanner +from bleak.backends.bluezdbus import defs +from bleak.backends.bluezdbus.characteristic import BleakGATTCharacteristicBlueZDBus +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.device import BLEDevice +from bleak.backends.service import BleakGATTServiceCollection +from bleak.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 logger = logging.getLogger(__name__) diff --git a/bleak/backends/bluezdbus/descriptor.py b/bleak/backends/bluezdbus/descriptor.py index 25dc7368d..e991528cc 100644 --- a/bleak/backends/bluezdbus/descriptor.py +++ b/bleak/backends/bluezdbus/descriptor.py @@ -5,8 +5,8 @@ if sys.platform != "linux": assert False, "This backend is only available on Linux" -from ..descriptor import BleakGATTDescriptor -from .defs import GattDescriptor1 +from bleak.backends.bluezdbus.defs import GattDescriptor1 +from bleak.backends.descriptor import BleakGATTDescriptor class BleakGATTDescriptorBlueZDBus(BleakGATTDescriptor): diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index 0a4d48b8b..ed4269edb 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -25,20 +25,28 @@ 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.backends.bluezdbus import defs +from bleak.backends.bluezdbus.advertisement_monitor import ( + AdvertisementMonitor, + OrPatternLike, +) +from bleak.backends.bluezdbus.characteristic import BleakGATTCharacteristicBlueZDBus +from bleak.backends.bluezdbus.defs import ( + Device1, + GattCharacteristic1, + GattDescriptor1, + GattService1, +) +from bleak.backends.bluezdbus.descriptor import BleakGATTDescriptorBlueZDBus +from bleak.backends.bluezdbus.service import BleakGATTServiceBlueZDBus +from bleak.backends.bluezdbus.signals import MatchRules, add_match +from bleak.backends.bluezdbus.utils import ( assert_reply, device_path_from_characteristic_path, get_dbus_authenticator, ) +from bleak.backends.service import BleakGATTServiceCollection +from bleak.exc import BleakDBusError, BleakError logger = logging.getLogger(__name__) diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index d5c633951..4da8c1b07 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -11,12 +11,16 @@ 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.backends.bluezdbus.advertisement_monitor import OrPatternLike +from bleak.backends.bluezdbus.defs import Device1 +from bleak.backends.bluezdbus.manager import get_global_bluez_manager +from bleak.backends.bluezdbus.utils import bdaddr_from_device_path +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BaseBleakScanner, +) +from bleak.exc import BleakError logger = logging.getLogger(__name__) diff --git a/bleak/backends/bluezdbus/service.py b/bleak/backends/bluezdbus/service.py index c73296ba2..5647a1bf4 100644 --- a/bleak/backends/bluezdbus/service.py +++ b/bleak/backends/bluezdbus/service.py @@ -7,9 +7,9 @@ from typing import Any -from ..service import BleakGATTService -from .characteristic import BleakGATTCharacteristicBlueZDBus -from .utils import extract_service_handle_from_path +from bleak.backends.bluezdbus.characteristic import BleakGATTCharacteristicBlueZDBus +from bleak.backends.bluezdbus.utils import extract_service_handle_from_path +from bleak.backends.service import BleakGATTService class BleakGATTServiceBlueZDBus(BleakGATTService): diff --git a/bleak/backends/bluezdbus/utils.py b/bleak/backends/bluezdbus/utils.py index 17d5dc7fa..74f09ec1d 100644 --- a/bleak/backends/bluezdbus/utils.py +++ b/bleak/backends/bluezdbus/utils.py @@ -12,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: diff --git a/bleak/backends/characteristic.py b/bleak/backends/characteristic.py index 4266cba30..2f47638c9 100644 --- a/bleak/backends/characteristic.py +++ b/bleak/backends/characteristic.py @@ -11,8 +11,8 @@ from typing import Any, Union from uuid import UUID -from ..uuids import uuidstr_to_str -from .descriptor import BleakGATTDescriptor +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.uuids import uuidstr_to_str class GattCharacteristicsFlags(enum.Enum): diff --git a/bleak/backends/client.py b/bleak/backends/client.py index ba773e413..1e2cbd41d 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -18,10 +18,10 @@ 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.device import BLEDevice +from bleak.backends.service import BleakGATTServiceCollection +from bleak.exc import BleakError NotifyCallback = Callable[[bytearray], None] diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 56eb432ab..74b7cf871 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -49,7 +49,7 @@ ) 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") diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index 79cb47bfe..a15559460 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -36,8 +36,8 @@ ) from Foundation import NSUUID, NSArray, NSData, NSError, NSNumber, NSObject, NSString -from ...exc import BleakError -from ..client import NotifyCallback +from bleak.backends.client import NotifyCallback +from bleak.exc import BleakError # logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) diff --git a/bleak/backends/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py index ebdf175c7..a76001bb3 100644 --- a/bleak/backends/corebluetooth/characteristic.py +++ b/bleak/backends/corebluetooth/characteristic.py @@ -18,10 +18,10 @@ from CoreBluetooth import CBCharacteristic -from ..characteristic import BleakGATTCharacteristic -from ..descriptor import BleakGATTDescriptor -from .descriptor import BleakGATTDescriptorCoreBluetooth -from .utils import cb_uuid_to_str +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.corebluetooth.descriptor import BleakGATTDescriptorCoreBluetooth +from bleak.backends.corebluetooth.utils import cb_uuid_to_str +from bleak.backends.descriptor import BleakGATTDescriptor class CBCharacteristicProperties(Enum): diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 15a74c97e..62a3f2246 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -30,23 +30,28 @@ ) from Foundation import NSArray, NSData -from ... import BleakScanner -from ...exc import ( +from bleak import BleakScanner +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, NotifyCallback +from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate +from bleak.backends.corebluetooth.characteristic import ( + BleakGATTCharacteristicCoreBluetooth, +) +from bleak.backends.corebluetooth.descriptor import BleakGATTDescriptorCoreBluetooth +from bleak.backends.corebluetooth.PeripheralDelegate import ( + NotificationDiscriminator, + PeripheralDelegate, +) +from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth +from bleak.backends.corebluetooth.service import BleakGATTServiceCoreBluetooth +from bleak.backends.corebluetooth.utils import cb_uuid_to_str +from bleak.backends.device import BLEDevice +from bleak.backends.service import BleakGATTServiceCollection +from bleak.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 NotificationDiscriminator, PeripheralDelegate -from .scanner import BleakScannerCoreBluetooth -from .service import BleakGATTServiceCoreBluetooth -from .utils import cb_uuid_to_str logger = logging.getLogger(__name__) diff --git a/bleak/backends/corebluetooth/descriptor.py b/bleak/backends/corebluetooth/descriptor.py index c87ff004c..1521d968e 100644 --- a/bleak/backends/corebluetooth/descriptor.py +++ b/bleak/backends/corebluetooth/descriptor.py @@ -14,8 +14,8 @@ from CoreBluetooth import CBDescriptor -from ..corebluetooth.utils import cb_uuid_to_str -from ..descriptor import BleakGATTDescriptor +from bleak.backends.corebluetooth.utils import cb_uuid_to_str +from bleak.backends.descriptor import BleakGATTDescriptor class BleakGATTDescriptorCoreBluetooth(BleakGATTDescriptor): diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index 403bbd50e..436c40563 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -12,10 +12,14 @@ 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 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__) diff --git a/bleak/backends/corebluetooth/service.py b/bleak/backends/corebluetooth/service.py index 2632ec321..06d40d5a0 100644 --- a/bleak/backends/corebluetooth/service.py +++ b/bleak/backends/corebluetooth/service.py @@ -7,9 +7,11 @@ from CoreBluetooth import CBService -from ..service import BleakGATTService -from .characteristic import BleakGATTCharacteristicCoreBluetooth -from .utils import cb_uuid_to_str +from bleak.backends.corebluetooth.characteristic import ( + BleakGATTCharacteristicCoreBluetooth, +) +from bleak.backends.corebluetooth.utils import cb_uuid_to_str +from bleak.backends.service import BleakGATTService class BleakGATTServiceCoreBluetooth(BleakGATTService): diff --git a/bleak/backends/corebluetooth/utils.py b/bleak/backends/corebluetooth/utils.py index 01f4f6ed0..a9079aeb6 100644 --- a/bleak/backends/corebluetooth/utils.py +++ b/bleak/backends/corebluetooth/utils.py @@ -8,7 +8,7 @@ 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: diff --git a/bleak/backends/descriptor.py b/bleak/backends/descriptor.py index 828ead569..f79cd3000 100644 --- a/bleak/backends/descriptor.py +++ b/bleak/backends/descriptor.py @@ -8,7 +8,7 @@ import abc from typing import Any -from ..uuids import normalize_uuid_16 +from bleak.uuids import normalize_uuid_16 _descriptor_descriptions = { normalize_uuid_16(0x2905): [ diff --git a/bleak/backends/p4android/characteristic.py b/bleak/backends/p4android/characteristic.py index 06cce6117..3edac49a9 100644 --- a/bleak/backends/p4android/characteristic.py +++ b/bleak/backends/p4android/characteristic.py @@ -9,10 +9,10 @@ from typing import Union from uuid import UUID -from ...exc import BleakError -from ..characteristic import BleakGATTCharacteristic -from ..descriptor import BleakGATTDescriptor -from . import defs +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.backends.p4android import defs +from bleak.exc import BleakError class BleakGATTCharacteristicP4Android(BleakGATTCharacteristic): diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index 3e1fa449d..dc57a8b72 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -18,15 +18,15 @@ 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.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, NotifyCallback +from bleak.backends.device import BLEDevice +from bleak.backends.p4android import defs, utils +from bleak.backends.p4android.characteristic import BleakGATTCharacteristicP4Android +from bleak.backends.p4android.descriptor import BleakGATTDescriptorP4Android +from bleak.backends.p4android.service import BleakGATTServiceP4Android +from bleak.backends.service import BleakGATTServiceCollection +from bleak.exc import BleakCharacteristicNotFoundError, BleakError logger = logging.getLogger(__name__) diff --git a/bleak/backends/p4android/descriptor.py b/bleak/backends/p4android/descriptor.py index 5a30b39ee..1ed8a3e14 100644 --- a/bleak/backends/p4android/descriptor.py +++ b/bleak/backends/p4android/descriptor.py @@ -5,7 +5,7 @@ if sys.platform != "android": assert False, "This backend is only available on Android" -from ..descriptor import BleakGATTDescriptor +from bleak.backends.descriptor import BleakGATTDescriptor class BleakGATTDescriptorP4Android(BleakGATTDescriptor): diff --git a/bleak/backends/p4android/scanner.py b/bleak/backends/p4android/scanner.py index 7689f0106..04e1673d3 100644 --- a/bleak/backends/p4android/scanner.py +++ b/bleak/backends/p4android/scanner.py @@ -19,9 +19,13 @@ 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__) diff --git a/bleak/backends/p4android/service.py b/bleak/backends/p4android/service.py index 363dd6e01..68991dc0a 100644 --- a/bleak/backends/p4android/service.py +++ b/bleak/backends/p4android/service.py @@ -5,8 +5,8 @@ if sys.platform != "android": assert False, "This backend is only available on Android" -from ..service import BleakGATTService -from .characteristic import BleakGATTCharacteristicP4Android +from bleak.backends.p4android.characteristic import BleakGATTCharacteristicP4Android +from bleak.backends.service import BleakGATTService class BleakGATTServiceP4Android(BleakGATTService): diff --git a/bleak/backends/p4android/utils.py b/bleak/backends/p4android/utils.py index a2ec0610f..0850acda8 100644 --- a/bleak/backends/p4android/utils.py +++ b/bleak/backends/p4android/utils.py @@ -11,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 7c3376dc1..424f66ed1 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -6,8 +6,8 @@ from collections.abc import Callable, Coroutine, Hashable from typing import Any, NamedTuple, Optional -from ..exc import BleakError -from .device import BLEDevice +from bleak.backends.device import BLEDevice +from bleak.exc import BleakError # prevent tasks from being garbage collected _background_tasks = set[asyncio.Task[None]]() diff --git a/bleak/backends/service.py b/bleak/backends/service.py index 85b9e0cfc..ff07329e1 100644 --- a/bleak/backends/service.py +++ b/bleak/backends/service.py @@ -11,10 +11,10 @@ from typing import Any, Optional, Union 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__) diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py index 51c1efb56..0d89d3c94 100644 --- a/bleak/backends/winrt/characteristic.py +++ b/bleak/backends/winrt/characteristic.py @@ -14,8 +14,8 @@ GattCharacteristicProperties, ) -from ..characteristic import BleakGATTCharacteristic -from ..descriptor import BleakGATTDescriptor +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.descriptor import BleakGATTDescriptor _GattCharacteristicsPropertiesMap = { GattCharacteristicProperties.NONE: ( diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index e023714e9..56b5493ef 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -78,21 +78,21 @@ ) from winrt.windows.storage.streams import Buffer as WinBuffer -from ... import BleakScanner -from ...exc import ( +from bleak import BleakScanner +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, NotifyCallback +from bleak.backends.device import BLEDevice +from bleak.backends.service import BleakGATTServiceCollection +from bleak.backends.winrt.characteristic import BleakGATTCharacteristicWinRT +from bleak.backends.winrt.descriptor import BleakGATTDescriptorWinRT +from bleak.backends.winrt.scanner import BleakScannerWinRT, _RawAdvData +from bleak.backends.winrt.service import BleakGATTServiceWinRT +from bleak.exc import ( PROTOCOL_ERROR_CODES, BleakCharacteristicNotFoundError, BleakDeviceNotFoundError, BleakError, ) -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, _RawAdvData -from .service import BleakGATTServiceWinRT logger = logging.getLogger(__name__) diff --git a/bleak/backends/winrt/descriptor.py b/bleak/backends/winrt/descriptor.py index 02bf76757..fb2c7c706 100644 --- a/bleak/backends/winrt/descriptor.py +++ b/bleak/backends/winrt/descriptor.py @@ -7,7 +7,7 @@ from winrt.windows.devices.bluetooth.genericattributeprofile import GattDescriptor -from ..descriptor import BleakGATTDescriptor +from bleak.backends.descriptor import BleakGATTDescriptor class BleakGATTDescriptorWinRT(BleakGATTDescriptor): diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index 533d06295..6e0cf4173 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -20,11 +20,15 @@ ) from winrt.windows.foundation import EventRegistrationToken -from ...assigned_numbers import AdvertisementDataType -from ...exc import BleakError -from ...uuids import normalize_uuid_str -from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner -from .util import assert_mta +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__) diff --git a/bleak/backends/winrt/service.py b/bleak/backends/winrt/service.py index 4635561af..6e18b4e77 100644 --- a/bleak/backends/winrt/service.py +++ b/bleak/backends/winrt/service.py @@ -7,8 +7,8 @@ from winrt.windows.devices.bluetooth.genericattributeprofile import GattDeviceService -from ..service import BleakGATTService -from ..winrt.characteristic import BleakGATTCharacteristicWinRT +from bleak.backends.service import BleakGATTService +from bleak.backends.winrt.characteristic import BleakGATTCharacteristicWinRT class BleakGATTServiceWinRT(BleakGATTService): diff --git a/bleak/backends/winrt/util.py b/bleak/backends/winrt/util.py index 68546f3e5..026649cb9 100644 --- a/bleak/backends/winrt/util.py +++ b/bleak/backends/winrt/util.py @@ -10,7 +10,7 @@ from ctypes import wintypes from enum import IntEnum -from ...exc import BleakError +from bleak.exc import BleakError if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout diff --git a/pyproject.toml b/pyproject.toml index 9ad11b6be..bf6e56c69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ build-backend = "poetry.core.masonry.api" [tool.isort] profile = "black" py_version=38 -src_paths = ["bleak", "examples", "tests", "typings"] +src_paths = ["bleak", "examples", "tests"] extend_skip = [".buildozer"] [tool.mypy] diff --git a/typings/CoreBluetooth/__init__.pyi b/typings/CoreBluetooth/__init__.pyi index 92cd8850c..cf137bf84 100644 --- a/typings/CoreBluetooth/__init__.pyi +++ b/typings/CoreBluetooth/__init__.pyi @@ -1,6 +1,6 @@ -from typing import Any, NewType, Optional, Type, TypeVar +from typing import Any, NewType, Optional, TypeVar -from ..Foundation import ( +from Foundation import ( NSUUID, NSArray, NSData, @@ -10,7 +10,7 @@ from ..Foundation import ( NSObject, NSString, ) -from ..libdispatch import dispatch_queue_t +from libdispatch import dispatch_queue_t class CBManager(NSObject): def state(self) -> CBManagerState: ... From ee9cc6b48c79db2daf8017a4610b2a8c924f4546 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 21:40:08 -0500 Subject: [PATCH 062/113] examples: strongly typed args namespace --- examples/async_callback_with_queue.py | 22 +++++++++++++++++----- examples/detection_callback.py | 10 ++++++++-- examples/disconnect_callback.py | 18 ++++++++++++++---- examples/discover.py | 9 +++++++-- examples/enable_notifications.py | 17 ++++++++++++++--- examples/service_explorer.py | 9 ++++++--- examples/two_devices.py | 12 +++++++++++- 7 files changed, 77 insertions(+), 20 deletions(-) diff --git a/examples/async_callback_with_queue.py b/examples/async_callback_with_queue.py index 955fbbd14..41bed2041 100644 --- a/examples/async_callback_with_queue.py +++ b/examples/async_callback_with_queue.py @@ -17,6 +17,7 @@ from typing import Optional from bleak import BleakClient, BleakScanner +from bleak.backends.characteristic import BleakGATTCharacteristic logger = logging.getLogger(__name__) @@ -25,8 +26,17 @@ class DeviceNotFoundError(Exception): pass +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: argparse.Namespace, queue: asyncio.Queue[tuple[float, Optional[bytearray]]] + args: Args, queue: asyncio.Queue[tuple[float, Optional[bytearray]]] ): logger.info("starting scan...") @@ -37,17 +47,19 @@ async def run_ble_client( 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) ) 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: bytearray) -> None: + async def callback_handler(_: BleakGATTCharacteristic, data: bytearray) -> None: await queue.put((time.time(), data)) async with BleakClient(device) as client: @@ -76,7 +88,7 @@ async def run_queue_consumer(queue: asyncio.Queue[tuple[float, Optional[bytearra logger.info("Received callback data via async queue at %s: %r", epoch, data) -async def main(args: argparse.Namespace): +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) @@ -124,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..1f91007bb 100644 --- a/examples/detection_callback.py +++ b/examples/detection_callback.py @@ -19,11 +19,17 @@ 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) -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) ) @@ -57,7 +63,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..6c8bff0b4 100644 --- a/examples/disconnect_callback.py +++ b/examples/disconnect_callback.py @@ -11,13 +11,21 @@ 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: @@ -27,17 +35,19 @@ async def main(args: argparse.Namespace): 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) ) 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..29ac00b21 100644 --- a/examples/discover.py +++ b/examples/discover.py @@ -14,7 +14,12 @@ 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( @@ -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..0db46146f 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,12 +20,20 @@ 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: @@ -34,13 +43,15 @@ async def main(args: argparse.Namespace): 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) ) 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/service_explorer.py b/examples/service_explorer.py index f28f3a39a..4646cb218 100644 --- a/examples/service_explorer.py +++ b/examples/service_explorer.py @@ -12,6 +12,7 @@ import argparse import asyncio import logging +from typing import Optional from bleak import BleakClient, BleakScanner @@ -19,8 +20,8 @@ class Args(argparse.Namespace): - name: str - address: str + name: Optional[str] + address: Optional[str] macos_use_bdaddr: bool services: list[str] pair: bool @@ -37,13 +38,15 @@ async def main(args: Args): 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) ) 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...") diff --git a/examples/two_devices.py b/examples/two_devices.py index d5fcab704..aa1a5f94c 100644 --- a/examples/two_devices.py +++ b/examples/two_devices.py @@ -8,6 +8,16 @@ 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( lock: asyncio.Lock, by_address: bool, @@ -144,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( From 3c777c2f9fe59faea20889964db7bf3ab6d4c203 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 22:01:06 -0500 Subject: [PATCH 063/113] bleak.args: add new sub-package for backend-specific args --- CHANGELOG.rst | 1 + bleak/__init__.py | 21 +--- bleak/args/__init__.py | 0 bleak/args/bluez.py | 99 +++++++++++++++++++ bleak/args/corebluetooth.py | 41 ++++++++ bleak/args/winrt.py | 32 ++++++ .../bluezdbus/advertisement_monitor.py | 20 +--- bleak/backends/bluezdbus/scanner.py | 78 +-------------- .../corebluetooth/PeripheralDelegate.py | 5 +- bleak/backends/corebluetooth/client.py | 23 +---- bleak/backends/corebluetooth/scanner.py | 18 +--- bleak/backends/winrt/client.py | 39 +------- docs/api/args.rst | 16 +++ docs/api/index.rst | 1 + 14 files changed, 207 insertions(+), 187 deletions(-) create mode 100644 bleak/args/__init__.py create mode 100644 bleak/args/bluez.py create mode 100644 bleak/args/corebluetooth.py create mode 100644 bleak/args/winrt.py create mode 100644 docs/api/args.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c33a448f8..928f81a6a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,7 @@ Changed * Updated ``winrt`` backend to use PyWinRT >= 3.1. * Added ``notification_discriminator`` parameter to start_notify on CoreBluetooth backend * Changed return type of ``connect()``, ``disconnect()``, ``pair()`` and ``unpair()`` methods to ``None``. +* Moved backend-specific arg types to new ``bleak.args`` sub-package. Fixed ----- diff --git a/bleak/__init__.py b/bleak/__init__.py index 67b16a1b0..4ff9fdf2e 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -16,16 +16,7 @@ import uuid from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable from types import TracebackType -from typing import ( - TYPE_CHECKING, - Any, - Literal, - Optional, - TypedDict, - Union, - cast, - overload, -) +from typing import Any, Literal, Optional, TypedDict, Union, cast, overload if sys.version_info < (3, 12): from typing_extensions import Buffer, Self @@ -40,6 +31,9 @@ from asyncio import timeout as async_timeout from typing import Never, 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.device import BLEDevice @@ -54,13 +48,6 @@ from bleak.exc import BleakCharacteristicNotFoundError, BleakError from bleak.uuids import normalize_uuid_str -if TYPE_CHECKING: - from bleak.backends.bluezdbus.scanner import BlueZScannerArgs - from bleak.backends.corebluetooth.client import CBStartNotifyArgs - from bleak.backends.corebluetooth.scanner import CBScannerArgs - from bleak.backends.winrt.client import WinRTClientArgs - - _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) if bool(os.environ.get("BLEAK_LOGGING", False)): 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/backends/bluezdbus/advertisement_monitor.py b/bleak/backends/bluezdbus/advertisement_monitor.py index f6f944d29..f98e0cd82 100644 --- a/bleak/backends/bluezdbus/advertisement_monitor.py +++ b/bleak/backends/bluezdbus/advertisement_monitor.py @@ -15,33 +15,17 @@ import logging from collections.abc import Iterable -from typing import NamedTuple, Union, no_type_check +from typing import no_type_check from dbus_fast import PropertyAccess from dbus_fast.service import ServiceInterface, dbus_property, method -from bleak.assigned_numbers import AdvertisementDataType +from bleak.args.bluez import OrPatternLike from bleak.backends.bluezdbus import defs logger = logging.getLogger(__name__) -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 AdvertisementMonitor(ServiceInterface): """ Implementation of the org.bluez.AdvertisementMonitor1 D-Bus interface. diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 4da8c1b07..a7fb42cd8 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -7,11 +7,11 @@ import logging from collections.abc import Callable, Coroutine -from typing import Any, Literal, Optional, TypedDict +from typing import Any, Literal, Optional from dbus_fast import Variant -from bleak.backends.bluezdbus.advertisement_monitor import OrPatternLike +from bleak.args.bluez import BlueZScannerArgs from bleak.backends.bluezdbus.defs import Device1 from bleak.backends.bluezdbus.manager import get_global_bluez_manager from bleak.backends.bluezdbus.utils import bdaddr_from_device_path @@ -25,80 +25,6 @@ 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. - - 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. - """ - - class BleakScannerBlueZDBus(BaseBleakScanner): """The native Linux Bleak BLE Scanner. diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index a15559460..337cf62fb 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -18,7 +18,7 @@ import asyncio import itertools import logging -from collections.abc import Callable, Iterable +from collections.abc import Iterable from typing import Any, NewType, Optional if sys.version_info < (3, 11): @@ -36,6 +36,7 @@ ) from Foundation import NSUUID, NSArray, NSData, NSError, NSNumber, NSObject, NSString +from bleak.args.corebluetooth import NotificationDiscriminator from bleak.backends.client import NotifyCallback from bleak.exc import BleakError @@ -43,8 +44,6 @@ logger = logging.getLogger(__name__) -NotificationDiscriminator = Callable[[bytes], bool] - CBPeripheralDelegate = objc.protocolNamed("CBPeripheralDelegate") CBCharacteristicWriteType = NewType("CBCharacteristicWriteType", int) diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 62a3f2246..9eb20ea4c 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -14,7 +14,7 @@ import asyncio import logging import uuid -from typing import Optional, TypedDict, Union +from typing import Optional, Union if sys.version_info < (3, 12): from typing_extensions import Buffer @@ -31,6 +31,7 @@ from Foundation import NSArray, NSData from bleak import BleakScanner +from bleak.args.corebluetooth import CBStartNotifyArgs from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient, NotifyCallback from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate @@ -38,10 +39,7 @@ BleakGATTCharacteristicCoreBluetooth, ) from bleak.backends.corebluetooth.descriptor import BleakGATTDescriptorCoreBluetooth -from bleak.backends.corebluetooth.PeripheralDelegate import ( - NotificationDiscriminator, - PeripheralDelegate, -) +from bleak.backends.corebluetooth.PeripheralDelegate import PeripheralDelegate from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth from bleak.backends.corebluetooth.service import BleakGATTServiceCoreBluetooth from bleak.backends.corebluetooth.utils import cb_uuid_to_str @@ -56,21 +54,6 @@ logger = logging.getLogger(__name__) -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. - """ - - class BleakClientCoreBluetooth(BaseBleakClient): """CoreBluetooth class interface for BleakClient diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index 436c40563..35e369dae 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -6,12 +6,13 @@ assert False, "This backend is only available on macOS" import logging -from typing import Any, Literal, Optional, TypedDict +from typing import Any, Literal, Optional import objc from CoreBluetooth import CBPeripheral from Foundation import NSBundle +from bleak.args.corebluetooth import CBScannerArgs from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate from bleak.backends.corebluetooth.utils import cb_uuid_to_str from bleak.backends.scanner import ( @@ -24,21 +25,6 @@ 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 `_. - """ - - class BleakScannerCoreBluetooth(BaseBleakScanner): """The native macOS Bleak BLE Scanner. diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 56b5493ef..f1971ed35 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -17,18 +17,7 @@ import logging import uuid from ctypes import WinError -from typing import ( - Any, - Generic, - Literal, - Optional, - Protocol, - Sequence, - TypedDict, - TypeVar, - Union, - cast, -) +from typing import Any, Generic, Optional, Protocol, Sequence, TypeVar, Union, cast if sys.version_info < (3, 12): from typing_extensions import Buffer, Self @@ -79,6 +68,7 @@ from winrt.windows.storage.streams import Buffer as WinBuffer from bleak import BleakScanner +from bleak.args.winrt import WinRTClientArgs from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient, NotifyCallback from bleak.backends.device import BLEDevice @@ -151,31 +141,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. 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/index.rst b/docs/api/index.rst index ce8d11fc3..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 From ba0df67c85e1d892a1a492ac34c0501b0bea48b8 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 22:18:42 -0500 Subject: [PATCH 064/113] examples: use {} instead of dict() for backend args Pyright was complaining about dict() being the wrong type. --- docs/backends/macos.rst | 2 +- examples/async_callback_with_queue.py | 4 ++-- examples/detection_callback.py | 2 +- examples/disconnect_callback.py | 4 ++-- examples/discover.py | 2 +- examples/enable_notifications.py | 4 ++-- examples/sensortag.py | 2 +- examples/service_explorer.py | 4 ++-- examples/two_devices.py | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/backends/macos.rst b/docs/backends/macos.rst index 410781400..a4abe5ed4 100644 --- a/docs/backends/macos.rst +++ b/docs/backends/macos.rst @@ -67,7 +67,7 @@ passed to the :meth:`bleak.BleakClient.start_notify` method that can differentia await client.start_notify( char, notification_handler, - cb=dict(notification_discriminator=notification_check_handler), + cb={"notification_discriminator": notification_check_handler}, ) while True: diff --git a/examples/async_callback_with_queue.py b/examples/async_callback_with_queue.py index 41bed2041..2a8e65d08 100644 --- a/examples/async_callback_with_queue.py +++ b/examples/async_callback_with_queue.py @@ -42,14 +42,14 @@ async def run_ble_client( 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 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) diff --git a/examples/detection_callback.py b/examples/detection_callback.py index 1f91007bb..76b5026c9 100644 --- a/examples/detection_callback.py +++ b/examples/detection_callback.py @@ -31,7 +31,7 @@ def simple_callback(device: BLEDevice, advertisement_data: AdvertisementData): 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: diff --git a/examples/disconnect_callback.py b/examples/disconnect_callback.py index 6c8bff0b4..b10e31195 100644 --- a/examples/disconnect_callback.py +++ b/examples/disconnect_callback.py @@ -30,14 +30,14 @@ async def main(args: Args): 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 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) diff --git a/examples/discover.py b/examples/discover.py index 29ac00b21..57e72bad3 100644 --- a/examples/discover.py +++ b/examples/discover.py @@ -25,7 +25,7 @@ async def main(args: Args): 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(): diff --git a/examples/enable_notifications.py b/examples/enable_notifications.py index 0db46146f..43de5ed74 100644 --- a/examples/enable_notifications.py +++ b/examples/enable_notifications.py @@ -38,14 +38,14 @@ async def main(args: Args): 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 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) diff --git a/examples/sensortag.py b/examples/sensortag.py index bfae5d627..94e8e94a8 100644 --- a/examples/sensortag.py +++ b/examples/sensortag.py @@ -82,7 +82,7 @@ async def main(address): - async with BleakClient(address, winrt=dict(use_cached_services=True)) as client: + 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) diff --git a/examples/service_explorer.py b/examples/service_explorer.py index 4646cb218..752d94ded 100644 --- a/examples/service_explorer.py +++ b/examples/service_explorer.py @@ -33,14 +33,14 @@ async def main(args: Args): 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 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) diff --git a/examples/two_devices.py b/examples/two_devices.py index aa1a5f94c..ef86d3724 100644 --- a/examples/two_devices.py +++ b/examples/two_devices.py @@ -55,7 +55,7 @@ async def connect_to_device( if by_address: device = await BleakScanner.find_device_by_address( - name_or_address, cb=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) From 5ac4c83929b34686b41d4b97b9a04f72fb4e846b Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 22:24:42 -0500 Subject: [PATCH 065/113] examples: more typing fixes now passes Pyright strict mode and mypy --- examples/mtu_size.py | 4 ++-- examples/philips_hue.py | 14 +++++++------- examples/sensortag.py | 7 +++++-- examples/uart_service.py | 2 ++ 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/examples/mtu_size.py b/examples/mtu_size.py index 426facf34..0b4d13766 100644 --- a/examples/mtu_size.py +++ b/examples/mtu_size.py @@ -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..23b3cdf96 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(protection_level=2) + 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 94e8e94a8..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,7 +82,7 @@ IO_CONFIG_CHAR_UUID = "f000aa66-0451-4000-b000-000000000000" -async def main(address): +async def main(address: str): async with BleakClient(address, winrt={"use_cached_services": True}) as client: print(f"Connected: {client.is_connected}") @@ -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/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. From 38924d736c833b0caba90046a425a5122bad072c Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 22:35:12 -0500 Subject: [PATCH 066/113] fix Self imports This was introduced in Python 3.11, not 3.12 --- bleak/__init__.py | 7 +++---- bleak/backends/winrt/client.py | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 4ff9fdf2e..0f63ae790 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -19,17 +19,16 @@ from typing import Any, Literal, Optional, TypedDict, Union, cast, overload if sys.version_info < (3, 12): - from typing_extensions import Buffer, Self + from typing_extensions import Buffer else: from collections.abc import Buffer - from typing import Self if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout - from typing_extensions import Never, Unpack, assert_never + from typing_extensions import Never, Self, Unpack, assert_never else: from asyncio import timeout as async_timeout - from typing import Never, Unpack, assert_never + from typing import Never, Self, Unpack, assert_never from bleak.args.bluez import BlueZScannerArgs from bleak.args.corebluetooth import CBScannerArgs, CBStartNotifyArgs diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index f1971ed35..64f89268f 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -20,16 +20,16 @@ from typing import Any, Generic, Optional, Protocol, Sequence, TypeVar, Union, cast if sys.version_info < (3, 12): - from typing_extensions import Buffer, Self + from typing_extensions import Buffer else: - from collections.abc import Buffer, Self + from collections.abc import Buffer if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout - from typing_extensions import assert_never + from typing_extensions import Self, assert_never else: from asyncio import timeout as async_timeout - from typing import assert_never + from typing import assert_never, Self from winrt.system import Object from winrt.windows.devices.bluetooth import ( From 6130c8038cb42bb5afd0cfd4106e40f9c9f156a4 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 22:41:30 -0500 Subject: [PATCH 067/113] backends: winrt: rename RawAdvData This is used in multiple files, so shouldn't be considered "private" --- bleak/backends/winrt/client.py | 8 ++++---- bleak/backends/winrt/scanner.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 64f89268f..8f14f6fa2 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -29,7 +29,7 @@ from typing_extensions import Self, assert_never else: from asyncio import timeout as async_timeout - from typing import assert_never, Self + from typing import Self, assert_never from winrt.system import Object from winrt.windows.devices.bluetooth import ( @@ -75,7 +75,7 @@ from bleak.backends.service import BleakGATTServiceCollection from bleak.backends.winrt.characteristic import BleakGATTCharacteristicWinRT from bleak.backends.winrt.descriptor import BleakGATTDescriptorWinRT -from bleak.backends.winrt.scanner import BleakScannerWinRT, _RawAdvData +from bleak.backends.winrt.scanner import BleakScannerWinRT, RawAdvData from bleak.backends.winrt.service import BleakGATTServiceWinRT from bleak.exc import ( PROTOCOL_ERROR_CODES, @@ -164,7 +164,7 @@ def __init__( # Backend specific. WinRT objects. if isinstance(address_or_ble_device, BLEDevice): - data: _RawAdvData = address_or_ble_device.details + data: RawAdvData = address_or_ble_device.details args = data.adv or data.scan assert args self._device_info = args.bluetooth_address @@ -235,7 +235,7 @@ async def connect(self, pair: bool, **kwargs: Any) -> None: self.address, f"Device with address {self.address} was not found." ) - data: _RawAdvData = device.details + data: RawAdvData = device.details args = data.adv or data.scan assert args self._device_info = args.bluetooth_address diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index 6e0cf4173..191d42f9d 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -44,7 +44,7 @@ def _format_event_args(e: BluetoothLEAdvertisementReceivedEventArgs) -> str: return _format_bdaddr(e.bluetooth_address) -class _RawAdvData(NamedTuple): +class RawAdvData(NamedTuple): """ Platform-specific advertisement data. @@ -89,7 +89,7 @@ def __init__( super(BleakScannerWinRT, self).__init__(detection_callback, service_uuids) self.watcher: Optional[BluetoothLEAdvertisementWatcher] = None - self._advertisement_pairs: dict[str, _RawAdvData] = {} + self._advertisement_pairs: dict[str, RawAdvData] = {} self._stopped_event: Optional[asyncio.Event] = None # case insensitivity is for backwards compatibility on Windows only @@ -129,13 +129,13 @@ 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 From a887f12a3b878e7ba522752521cafe529add3e25 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 22:53:19 -0500 Subject: [PATCH 068/113] typing: ignore PlatformBleakScanner constructor complaints Inheritance of __init__ doesn't follow normal rules, so the type checker isn't liking it. --- bleak/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 0f63ae790..acca004cc 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -129,11 +129,11 @@ 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) -> Self: await self._backend.start() From 6ed559c2faf0b637fc5275df915fc911caeb2e3e Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 17 May 2025 23:52:15 -0500 Subject: [PATCH 069/113] typings/Foundation: add NSBundle --- typings/Foundation/__init__.pyi | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/typings/Foundation/__init__.pyi b/typings/Foundation/__init__.pyi index 7996e18d9..b65378744 100644 --- a/typings/Foundation/__init__.pyi +++ b/typings/Foundation/__init__.pyi @@ -30,6 +30,11 @@ class NSArray(NSObject): class NSValue(NSObject): ... class NSNumber(NSValue): ... +class NSBundle(NSObject): + @classmethod + def mainBundle(cls) -> NSBundle: ... + def bundleIdentifier(self) -> NSString: ... + NSKeyValueObservingOptions = NewType("NSKeyValueObservingOptions", int) NSKeyValueObservingOptionNew: NSKeyValueObservingOptions NSKeyValueObservingOptionOld: NSKeyValueObservingOptions From 5af0c7ab8ea5edfe58342a004989eab22b757c28 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 00:11:17 -0500 Subject: [PATCH 070/113] backends: get_services is no longer a required method Drop the method from the abstract base class and make it private in the backend implementations. --- bleak/backends/bluezdbus/client.py | 6 +++--- bleak/backends/client.py | 12 ------------ bleak/backends/corebluetooth/client.py | 4 ++-- bleak/backends/p4android/client.py | 4 ++-- bleak/backends/winrt/client.py | 6 +++--- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index f27823b0d..732cace6f 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -208,7 +208,7 @@ def on_value_changed(char_path: str, value: bytes) -> None: async def disconnect_device() -> None: # Calling Disconnect cancels any pending connect request. Also, - # if connection was successful but get_services() raises (e.g. + # 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: @@ -345,7 +345,7 @@ async def disconnect_device() -> None: # We will try to use the cache if it exists and `dangerous_use_bleak_cache` # is True. - await self.get_services( + await self._get_services( dangerous_use_bleak_cache=dangerous_use_bleak_cache ) @@ -654,7 +654,7 @@ def mtu_size(self) -> int: # GATT services methods - async def get_services( + async def _get_services( self, dangerous_use_bleak_cache: bool = False, **kwargs: Any ) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. diff --git a/bleak/backends/client.py b/bleak/backends/client.py index 1e2cbd41d..5c9cbcada 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -115,18 +115,6 @@ def is_connected(self) -> bool: """ raise NotImplementedError() - # GATT services methods - - @abc.abstractmethod - async def get_services(self, **kwargs: Any) -> 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 diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 9eb20ea4c..03f9c1841 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -144,7 +144,7 @@ def disconnect_callback() -> None: await manager.connect(self._peripheral, disconnect_callback, timeout=timeout) # Now get services - await self.get_services() + await self._get_services() async def disconnect(self) -> None: """Disconnect from the peripheral device""" @@ -206,7 +206,7 @@ async def unpair(self) -> None: """ raise NotImplementedError("Pairing is not available in Core Bluetooth.") - async def get_services(self, **kwargs) -> BleakGATTServiceCollection: + async def _get_services(self, **kwargs) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. Returns: diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index dc57a8b72..292792585 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -117,7 +117,7 @@ async def connect(self, pair: bool, **kwargs) -> None: 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 @@ -230,7 +230,7 @@ def mtu_size(self) -> Optional[int]: # GATT services methods - async def get_services(self) -> BleakGATTServiceCollection: + async def _get_services(self) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. Returns: diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 8f14f6fa2..aed958ae0 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -402,7 +402,7 @@ def max_pdu_size_changed_handler(sender: GattSession, args: Object) -> None: ) get_services_task = asyncio.create_task( - self.get_services( + self._get_services( service_cache_mode=service_cache_mode, cache_mode=cache_mode, ) @@ -437,7 +437,7 @@ def max_pdu_size_changed_handler(sender: GattSession, args: Object) -> None: 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, ) @@ -613,7 +613,7 @@ async def unpair(self) -> None: # GATT services methods - async def get_services( + async def _get_services( self, *, service_cache_mode: Optional[BluetoothCacheMode] = None, From b7d963bd11843e59beaec8f058a1ce7bfe4e78fa Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 10:42:08 -0500 Subject: [PATCH 071/113] backends: client: use @override decorator Make it clear which methods are overriding the base class. --- bleak/backends/bluezdbus/client.py | 15 ++++++++++++++- bleak/backends/corebluetooth/client.py | 15 ++++++++++++++- bleak/backends/p4android/client.py | 19 ++++++++++++++++++- bleak/backends/winrt/client.py | 15 ++++++++++++++- 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 732cace6f..6f5727068 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -19,9 +19,10 @@ from uuid import UUID 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 @@ -112,6 +113,7 @@ def __init__( # Connectivity methods + @override async def connect( self, pair: bool, dangerous_use_bleak_cache: bool = False, **kwargs: Any ) -> None: @@ -415,6 +417,7 @@ def _cleanup_all(self) -> None: # Reset all stored services. self.services = None + @override async def disconnect(self) -> None: """Disconnect from the specified GATT server. @@ -459,6 +462,7 @@ async def disconnect(self) -> None: # "PropertiesChanged" signal handler and that it completed successfully assert self._bus is None + @override async def pair(self, *args: Any, **kwargs: Any) -> None: """Pair with the peripheral. @@ -510,6 +514,7 @@ async def pair(self, *args: Any, **kwargs: Any) -> None: assert reply assert_reply(reply) + @override async def unpair(self) -> None: """Unpair with the peripheral.""" adapter_path = await self._get_adapter_path() @@ -555,6 +560,7 @@ async def unpair(self) -> None: raise @property + @override def is_connected(self) -> bool: """Check connection status between this client and the server. @@ -642,6 +648,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: @@ -682,6 +689,7 @@ async def _get_services( # IO methods + @override async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID], @@ -755,6 +763,7 @@ async def read_gatt_char( ) return value + @override async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: """Perform read operation on the specified GATT descriptor. @@ -803,6 +812,7 @@ async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: logger.debug("Read Descriptor %s | %s: %s", handle, descriptor.path, value) return value + @override async def write_gatt_char( self, characteristic: BleakGATTCharacteristic, @@ -848,6 +858,7 @@ async def write_gatt_char( data, ) + @override async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: """Perform a write operation on the specified GATT descriptor. @@ -892,6 +903,7 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: logger.debug("Write Descriptor %s | %s: %s", handle, descriptor.path, data) + @override async def start_notify( self, characteristic: BleakGATTCharacteristic, @@ -918,6 +930,7 @@ async def start_notify( assert reply assert_reply(reply) + @override async def stop_notify( self, char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID], diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 03f9c1841..58b3cb289 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -17,9 +17,10 @@ from typing import 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, @@ -93,6 +94,7 @@ def __init__( def __str__(self) -> str: return "BleakClientCoreBluetooth ({})".format(self.address) + @override async def connect(self, pair: bool, **kwargs) -> None: """Connect to a specified Peripheral @@ -146,6 +148,7 @@ def disconnect_callback() -> None: # Now get services await self._get_services() + @override async def disconnect(self) -> None: """Disconnect from the peripheral device""" if ( @@ -157,6 +160,7 @@ async def disconnect(self) -> None: await self._central_manager_delegate.disconnect(self._peripheral) @property + @override def is_connected(self) -> bool: """Checks for current active connection""" return ( @@ -166,6 +170,7 @@ def is_connected(self) -> bool: ) @property + @override def mtu_size(self) -> int: """Get ATT MTU size for active connection""" # Use type CBCharacteristicWriteWithoutResponse to get maximum write @@ -178,6 +183,7 @@ def mtu_size(self) -> int: + 3 ) + @override async def pair(self, *args, **kwargs) -> None: """Attempt to pair with a peripheral. @@ -195,6 +201,7 @@ async def pair(self, *args, **kwargs) -> None: """ raise NotImplementedError("Pairing is not available in Core Bluetooth.") + @override async def unpair(self) -> None: """ Remove pairing information for a peripheral. @@ -257,6 +264,7 @@ async def _get_services(self, **kwargs) -> BleakGATTServiceCollection: self.services = services return self.services + @override async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], @@ -290,6 +298,7 @@ async def read_gatt_char( 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 ) -> bytearray: @@ -319,6 +328,7 @@ async def read_gatt_descriptor( logger.debug("Read Descriptor {0} : {1}".format(handle, value)) return value + @override async def write_gatt_char( self, characteristic: BleakGATTCharacteristic, @@ -337,6 +347,7 @@ async def write_gatt_char( ) logger.debug(f"Write Characteristic {characteristic.uuid} : {data}") + @override async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: """Perform a write operation on the specified GATT descriptor. @@ -353,6 +364,7 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: await self._delegate.write_descriptor(descriptor.obj, value) logger.debug("Write Descriptor {0} : {1}".format(handle, data)) + @override async def start_notify( self, characteristic: BleakGATTCharacteristic, @@ -372,6 +384,7 @@ async def start_notify( cb.get("notification_discriminator"), ) + @override async def stop_notify( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID] ) -> None: diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index 292792585..5d165deb8 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -15,6 +15,11 @@ import warnings from typing import 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 @@ -64,6 +69,7 @@ def __del__(self): # Connectivity methods + @override async def connect(self, pair: bool, **kwargs) -> None: """Connect to the specified GATT server.""" if pair: @@ -127,6 +133,7 @@ async def connect(self, pair: bool, **kwargs) -> None: pass raise + @override async def disconnect(self) -> None: """Disconnect from the specified GATT server.""" logger.debug("Disconnecting from BLE device...") @@ -157,6 +164,7 @@ async def disconnect(self) -> None: # Reset all stored services. self.services = None + @override async def pair(self, *args, **kwargs) -> None: """Pair with the peripheral. @@ -204,6 +212,7 @@ def handleBondStateChanged(context, intent): finally: await receiver.stop() + @override async def unpair(self) -> None: """Unpair with the peripheral.""" warnings.warn( @@ -211,6 +220,7 @@ async def unpair(self) -> None: ) @property + @override def is_connected(self) -> bool: """Check connection status between this client and the server. @@ -225,7 +235,8 @@ def is_connected(self) -> bool: ) @property - def mtu_size(self) -> Optional[int]: + @override + def mtu_size(self) -> int: return self.__mtu # GATT services methods @@ -280,6 +291,7 @@ async def _get_services(self) -> BleakGATTServiceCollection: # IO methods + @override async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristicP4Android, int, str, uuid.UUID], @@ -315,6 +327,7 @@ async def read_gatt_char( ) return value + @override async def read_gatt_descriptor( self, desc_specifier: Union[BleakGATTDescriptorP4Android, str, uuid.UUID], @@ -352,6 +365,7 @@ async def read_gatt_descriptor( return value + @override async def write_gatt_char( self, characteristic: BleakGATTCharacteristic, @@ -379,6 +393,7 @@ 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], @@ -413,6 +428,7 @@ async def write_gatt_descriptor( f"Write Descriptor {descriptor.uuid} | {descriptor.handle}: {data}" ) + @override async def start_notify( self, characteristic: BleakGATTCharacteristic, @@ -436,6 +452,7 @@ async def start_notify( defs.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, ) + @override async def stop_notify( self, char_specifier: Union[BleakGATTCharacteristicP4Android, int, str, uuid.UUID], diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index aed958ae0..e78951d05 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -20,9 +20,10 @@ from typing import Any, Generic, Optional, Protocol, Sequence, TypeVar, Union, cast 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 @@ -217,6 +218,7 @@ async def _create_requester(self, bluetooth_address: int) -> BluetoothLEDevice: ) return requester + @override async def connect(self, pair: bool, **kwargs: Any) -> None: """Connect to the specified GATT server. @@ -457,6 +459,7 @@ def max_pdu_size_changed_handler(sender: GattSession, args: Object) -> None: finally: self._session_active_events.remove(event) + @override async def disconnect(self) -> None: """Disconnect from the specified GATT server.""" logger.debug("Disconnecting from BLE device...") @@ -504,6 +507,7 @@ async def disconnect(self) -> None: self._session_closed_events.remove(event) @property + @override def is_connected(self) -> bool: """Check connection status between this client and the server. @@ -518,10 +522,12 @@ def is_connected(self) -> bool: ) @property + @override def mtu_size(self) -> int: """Get ATT MTU size for active connection""" return self._session.max_pdu_size + @override async def pair( self, protection_level: Optional[DevicePairingProtectionLevel] = None, @@ -587,6 +593,7 @@ def handler( pairing_result.protection_level_used, ) + @override async def unpair(self) -> None: """Attempts to unpair from the device. @@ -756,6 +763,7 @@ async def _get_services( # I/O methods + @override async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], @@ -808,6 +816,7 @@ async def read_gatt_char( return value + @override async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: """Perform read operation on the specified GATT descriptor. @@ -851,6 +860,7 @@ async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: return value + @override async def write_gatt_char( self, characteristic: BleakGATTCharacteristic, @@ -881,6 +891,7 @@ async def write_gatt_char( f"Could not write value {data} to characteristic {characteristic.handle:04X}", ) + @override async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: """Perform a write operation on the specified GATT descriptor. @@ -914,6 +925,7 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: logger.debug("Write Descriptor %04X : %s", handle, data) + @override async def start_notify( self, characteristic: BleakGATTCharacteristic, @@ -974,6 +986,7 @@ def handle_value_changed( raise + @override async def stop_notify( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID] ) -> None: From d46a8883c247506182dcfd8b400d73fc1b72ff57 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 10:51:34 -0500 Subject: [PATCH 072/113] backends: scanner: remove set_scanning_filter() method This was not implemented on most backends. BlueZ handles it during the start scan method. --- bleak/backends/corebluetooth/scanner.py | 16 ---------------- bleak/backends/p4android/scanner.py | 5 ----- bleak/backends/scanner.py | 10 ---------- bleak/backends/winrt/scanner.py | 19 ------------------- 4 files changed, 50 deletions(-) diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index 35e369dae..d290f05a7 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -155,22 +155,6 @@ 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 diff --git a/bleak/backends/p4android/scanner.py b/bleak/backends/p4android/scanner.py index 04e1673d3..3646bc953 100644 --- a/bleak/backends/p4android/scanner.py +++ b/bleak/backends/p4android/scanner.py @@ -228,11 +228,6 @@ def __stop(self) -> None: 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() diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 424f66ed1..89fcc7991 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -272,16 +272,6 @@ async def stop(self) -> None: """Stop scanning for devices""" raise NotImplementedError() - @abc.abstractmethod - def set_scanning_filter(self, **kwargs: Any) -> 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]: """ diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index 191d42f9d..eaa57615d 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -302,22 +302,3 @@ async def stop(self) -> None: self._received_token = None self.watcher = None - - def set_scanning_filter(self, **kwargs: Any) -> 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"] From cb1cd8f0b94f2820fe9f14f0f5236386a45d73fa Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 10:55:19 -0500 Subject: [PATCH 073/113] backends: scanner: use override decorator Indicate overridden methods from base class. --- bleak/backends/bluezdbus/scanner.py | 7 +++++++ bleak/backends/corebluetooth/scanner.py | 7 +++++++ bleak/backends/p4android/scanner.py | 7 +++++++ bleak/backends/winrt/scanner.py | 7 +++++++ 4 files changed, 28 insertions(+) diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index a7fb42cd8..3e0ae3530 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -9,6 +9,11 @@ from collections.abc import Callable, Coroutine from typing import Any, Literal, Optional +if sys.version_info < (3, 12): + from typing_extensions import override +else: + from typing import override + from dbus_fast import Variant from bleak.args.bluez import BlueZScannerArgs @@ -92,6 +97,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() @@ -117,6 +123,7 @@ async def start(self) -> None: self._handle_device_removed, ) + @override async def stop(self) -> None: if self._stop: # avoid reentrancy diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index d290f05a7..7657c5888 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -8,6 +8,11 @@ import logging from typing import Any, Literal, Optional +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 @@ -84,6 +89,7 @@ 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 = {} @@ -151,6 +157,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) diff --git a/bleak/backends/p4android/scanner.py b/bleak/backends/p4android/scanner.py index 3646bc953..0778be556 100644 --- a/bleak/backends/p4android/scanner.py +++ b/bleak/backends/p4android/scanner.py @@ -15,6 +15,11 @@ 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 @@ -69,6 +74,7 @@ def __init__( 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.") @@ -225,6 +231,7 @@ def __stop(self) -> None: else: logger.debug("BTLE scan already stopped") + @override async def stop(self) -> None: self.__stop() diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index eaa57615d..053995e4d 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -10,6 +10,11 @@ from typing import Literal, NamedTuple, Optional from uuid import UUID +if sys.version_info < (3, 12): + from typing_extensions import override +else: + from typing import override + from winrt.windows.devices.bluetooth.advertisement import ( BluetoothLEAdvertisementReceivedEventArgs, BluetoothLEAdvertisementType, @@ -225,6 +230,7 @@ def _stopped_handler( assert self._stopped_event self._stopped_event.set() + @override async def start(self) -> None: if self.watcher: raise BleakError("Scanner already started") @@ -276,6 +282,7 @@ def on_stopped( 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 From d98b9240c9160109645e23ae65293577213ada5e Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 10:57:33 -0500 Subject: [PATCH 074/113] backends: android: drop __del__ methods Other backends don't have this, so android shouldn't either. --- bleak/backends/p4android/client.py | 5 ----- bleak/backends/p4android/scanner.py | 10 ++-------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index 5d165deb8..61d1b9fc1 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -62,11 +62,6 @@ 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 @override diff --git a/bleak/backends/p4android/scanner.py b/bleak/backends/p4android/scanner.py index 0778be556..6db880b85 100644 --- a/bleak/backends/p4android/scanner.py +++ b/bleak/backends/p4android/scanner.py @@ -71,9 +71,6 @@ 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: @@ -222,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) @@ -231,10 +229,6 @@ def __stop(self) -> None: else: logger.debug("BTLE scan already stopped") - @override - async def stop(self) -> None: - self.__stop() - def _handle_scan_result(self, result) -> None: native_device = result.getDevice() record = result.getScanRecord() From 8fc5eb93fce64d48124f0e3f89006cc6030fb6fe Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 11:29:05 -0500 Subject: [PATCH 075/113] examples: include device name in info --- examples/detection_callback.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/detection_callback.py b/examples/detection_callback.py index 76b5026c9..c848ec2cb 100644 --- a/examples/detection_callback.py +++ b/examples/detection_callback.py @@ -26,7 +26,9 @@ class Args(argparse.Namespace): 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: Args): From 127bd4f70102f12b6661fbb6b3b4c5cd8490a393 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 11:31:42 -0500 Subject: [PATCH 076/113] backends: scanner: fix type of device name The name can be None. --- bleak/backends/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 89fcc7991..a329868ee 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -236,7 +236,7 @@ def call_detection_callbacks( callback(device, advertisement_data) def create_or_update_device( - self, address: str, name: str, details: Any, adv: AdvertisementData + self, address: str, name: Optional[str], details: Any, adv: AdvertisementData ) -> BLEDevice: """ Creates or updates a device in :attr:`seen_devices`. From cd9862cf5d7f6e033f91f8f3220b11a171f05a31 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 12:33:45 -0500 Subject: [PATCH 077/113] backends: bluez: use None for name instead of address BlueZ will use the address of a Bluetooth device as the name if it doesn't have a name. All other platforms will use None. So to be consistent, make the BlueZ backend use None as well. --- CHANGELOG.rst | 1 + bleak/backends/bluezdbus/scanner.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 928f81a6a..9e6749ea4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,7 @@ Changed * Added ``notification_discriminator`` parameter to start_notify on CoreBluetooth backend * 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. Fixed ----- diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 3e0ae3530..a0069153f 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -200,7 +200,13 @@ def _handle_advertising_data(self, path: str, props: Device1) -> None: device = self.create_or_update_device( 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, ) From 9c11c6eb696c94883710044e79dec9a084feec67 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 12:40:00 -0500 Subject: [PATCH 078/113] CHANGELOG: clean up a bit --- CHANGELOG.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e6749ea4..b77507849 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,14 +13,14 @@ and this project adheres to `Semantic Versioning =2.0``. -* Log to stderr instead of stdout when ``BLEAK_LOGGING`` is enabled. +* Updated Poetry build system version to ``>=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. -* Added ``notification_discriminator`` parameter to start_notify on CoreBluetooth backend * 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. @@ -36,7 +36,7 @@ Fixed Removed ------- * Removed support for Python 3.8. The minimum supported version is now Python 3.9. -* Removed deprecated parameters and methods. +* Removed deprecated parameters properties and methods. * Removed support for macOS < 10.13. * Removed support for BlueZ < 5.55. From 579a8a6b087ca54ed95ec864011dfa37a3c38db7 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 14:33:07 -0500 Subject: [PATCH 079/113] backends: bluezdbus: fix regression in connect() method Commit 28e4f53 removed a return statement that broke the connect() method. Put it back. --- bleak/backends/bluezdbus/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 6f5727068..dc36ad895 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -352,6 +352,7 @@ async def disconnect_device() -> None: ) stack.pop_all() + return @staticmethod async def _disconnect_monitor( From 2fc8636ff4ae9e6c015e343f332e2e3f95c5b44a Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 17:55:44 -0500 Subject: [PATCH 080/113] backends: corebluetooth: improve typings Also remove some dead code. --- .../corebluetooth/CentralManagerDelegate.py | 25 ++--- .../corebluetooth/PeripheralDelegate.py | 61 +++++++---- bleak/backends/corebluetooth/client.py | 28 +++-- bleak/backends/corebluetooth/scanner.py | 20 ++-- bleak/backends/corebluetooth/utils.py | 24 ----- typings/CoreBluetooth/__init__.pyi | 102 ++++++++++-------- typings/Foundation/__init__.pyi | 51 ++++++--- typings/objc/__init__.pyi | 14 ++- 8 files changed, 181 insertions(+), 144 deletions(-) diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 74b7cf871..f9b0cc771 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -21,8 +21,10 @@ 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 ( @@ -43,9 +45,7 @@ NSError, NSKeyValueChangeNewKey, NSKeyValueObservingOptionNew, - NSNumber, NSObject, - NSString, ) from libdispatch import DISPATCH_QUEUE_SERIAL, dispatch_queue_create @@ -63,7 +63,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() @@ -74,7 +74,8 @@ def init(self) -> Optional["CentralManagerDelegate"]: self._connect_futures: dict[NSUUID, asyncio.Future[bool]] = {} self.callbacks: dict[ - int, Callable[[CBPeripheral, dict[str, Any], int], None] + int, + Callable[[CBPeripheral, NSDictionary, int], None], ] = {} self._disconnect_callbacks: dict[NSUUID, DisconnectCallback] = {} self._disconnect_futures: dict[NSUUID, asyncio.Future[None]] = {} @@ -121,16 +122,16 @@ def __del__(self) -> None: @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)) - ) + _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 ) event = asyncio.Event() @@ -201,7 +202,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) @@ -236,7 +237,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 @@ -272,7 +273,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 337cf62fb..5ba22297e 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -19,7 +19,7 @@ import itertools import logging from collections.abc import Iterable -from typing import Any, NewType, Optional +from typing import Any, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout @@ -28,13 +28,15 @@ 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 bleak.args.corebluetooth import NotificationDiscriminator from bleak.backends.client import NotifyCallback @@ -46,8 +48,6 @@ CBPeripheralDelegate = objc.protocolNamed("CBPeripheralDelegate") -CBCharacteristicWriteType = NewType("CBCharacteristicWriteType", int) - class PeripheralDelegate(NSObject): """macOS conforming python class for managing the PeripheralDelegate for BLE""" @@ -70,7 +70,7 @@ def initWithPeripheral_( self._services_discovered_future = self._event_loop.create_future() self._service_characteristic_discovered_futures: dict[ - int, asyncio.Future[NSArray] + int, asyncio.Future[NSArray[CBCharacteristic]] ] = {} self._characteristic_descriptor_discover_futures: dict[ int, asyncio.Future[None] @@ -88,7 +88,7 @@ def initWithPeripheral_( int, Optional[NotificationDiscriminator] ] = {} - self._read_rssi_futures: dict[NSUUID, asyncio.Future[NSNumber]] = {} + self._read_rssi_futures: dict[NSUUID, asyncio.Future[int]] = {} return self @@ -118,7 +118,9 @@ def futures(self) -> Iterable[asyncio.Future[Any]]: ) @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 @@ -129,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 @@ -140,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()] = ( @@ -163,8 +169,9 @@ async def read_characteristic( use_cached: bool = True, 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() @@ -181,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() @@ -273,7 +281,7 @@ async def stop_notifications(self, characteristic: CBCharacteristic) -> None: 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 @@ -287,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: @@ -313,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( @@ -388,7 +399,7 @@ def did_update_value_for_characteristic( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, - value: NSData, + value: Optional[NSData], error: Optional[NSError], ) -> None: c_handle = characteristic.handle() @@ -401,6 +412,8 @@ def did_update_value_for_characteristic( # 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) ) @@ -427,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_( @@ -449,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()) @@ -463,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_( @@ -586,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) @@ -603,7 +618,7 @@ def did_read_rssi( def peripheral_didReadRSSI_error_( self: PeripheralDelegate, peripheral: CBPeripheral, - rssi: NSNumber, + rssi: int, error: Optional[NSError], ) -> None: logger.debug("peripheral_didReadRSSI_error_") @@ -614,7 +629,7 @@ def peripheral_didReadRSSI_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: @@ -625,14 +640,14 @@ 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( diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 58b3cb289..a4ca283d0 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -5,7 +5,7 @@ """ import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: if sys.platform != "darwin": @@ -71,7 +71,7 @@ def __init__( self, address_or_ble_device: Union[BLEDevice, str], services: Optional[set[str]] = None, - **kwargs, + **kwargs: Any, ): super(BleakClientCoreBluetooth, self).__init__(address_or_ble_device, **kwargs) @@ -86,7 +86,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 ) @@ -95,7 +97,7 @@ def __str__(self) -> str: return "BleakClientCoreBluetooth ({})".format(self.address) @override - async def connect(self, pair: bool, **kwargs) -> None: + async def connect(self, pair: bool, **kwargs: Any) -> None: """Connect to a specified Peripheral Keyword Args: @@ -157,6 +159,7 @@ async def disconnect(self) -> None: ): return + assert self._central_manager_delegate await self._central_manager_delegate.disconnect(self._peripheral) @property @@ -176,6 +179,7 @@ def mtu_size(self) -> int: # 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 @@ -184,7 +188,7 @@ def mtu_size(self) -> int: ) @override - async def pair(self, *args, **kwargs) -> None: + async def pair(self, *args: Any, **kwargs: Any) -> None: """Attempt to pair with a peripheral. Raises: @@ -213,7 +217,7 @@ async def unpair(self) -> None: """ 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: @@ -226,6 +230,7 @@ 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: @@ -269,7 +274,7 @@ async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], use_cached: bool = False, - **kwargs, + **kwargs: Any, ) -> bytearray: """Perform read operation on the specified GATT characteristic. @@ -291,6 +296,7 @@ async def read_gatt_char( if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) + assert self._delegate output = await self._delegate.read_characteristic( characteristic.obj, use_cached=use_cached ) @@ -300,7 +306,7 @@ async def read_gatt_char( @override async def read_gatt_descriptor( - self, handle: int, use_cached: bool = False, **kwargs + self, handle: int, use_cached: bool = False, **kwargs: Any ) -> bytearray: """Perform read operation on the specified GATT descriptor. @@ -316,6 +322,7 @@ async def read_gatt_descriptor( 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 ) @@ -360,6 +367,7 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: 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)) @@ -371,7 +379,7 @@ async def start_notify( callback: NotifyCallback, *, cb: CBStartNotifyArgs, - **kwargs, + **kwargs: Any, ) -> None: """ Activate notifications/indications on a characteristic. @@ -404,8 +412,10 @@ async def stop_notify( 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/scanner.py b/bleak/backends/corebluetooth/scanner.py index 7657c5888..a55bc4126 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -15,7 +15,7 @@ import objc from CoreBluetooth import CBPeripheral -from Foundation import NSBundle +from Foundation import NSBundle, NSDictionary from bleak.args.corebluetooth import CBScannerArgs from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate @@ -65,7 +65,7 @@ def __init__( scanning_mode: Literal["active", "passive"], *, cb: CBScannerArgs, - **kwargs, + **kwargs: Any, ): super(BleakScannerCoreBluetooth, self).__init__( detection_callback, service_uuids @@ -76,7 +76,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) @@ -93,7 +95,7 @@ def __init__( 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", []) @@ -110,7 +112,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" @@ -133,7 +135,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: @@ -161,9 +163,3 @@ def callback(p: CBPeripheral, a: dict[str, Any], r: int) -> None: async def stop(self) -> None: await self._manager.stop_scan() self._manager.callbacks.pop(id(self), None) - - # macOS specific methods - - @property - def is_scanning(self): - return self._manager.isScanning_ diff --git a/bleak/backends/corebluetooth/utils.py b/bleak/backends/corebluetooth/utils.py index a9079aeb6..d462c4aba 100644 --- a/bleak/backends/corebluetooth/utils.py +++ b/bleak/backends/corebluetooth/utils.py @@ -6,7 +6,6 @@ assert False, "This backend is only available on macOS" from CoreBluetooth import CBUUID -from Foundation import NSData from bleak.uuids import normalize_uuid_str @@ -24,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/typings/CoreBluetooth/__init__.pyi b/typings/CoreBluetooth/__init__.pyi index cf137bf84..b484683d2 100644 --- a/typings/CoreBluetooth/__init__.pyi +++ b/typings/CoreBluetooth/__init__.pyi @@ -1,15 +1,12 @@ -from typing import Any, NewType, Optional, TypeVar - -from Foundation import ( - NSUUID, - NSArray, - NSData, - NSDictionary, - NSError, - NSNumber, - NSObject, - NSString, -) +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): @@ -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 b65378744..b4aa70cf5 100644 --- a/typings/Foundation/__init__.pyi +++ b/typings/Foundation/__init__.pyi @@ -1,39 +1,58 @@ -from collections.abc import Sequence +import sys +from collections.abc import Mapping, Sequence from typing import Any, NewType, Optional, TypeVar +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]): ... + +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[Any]) -> 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]): + def initWithArray_(self, array: Sequence[Any]) -> Self: ... class NSValue(NSObject): ... -class NSNumber(NSValue): ... class NSBundle(NSObject): @classmethod def mainBundle(cls) -> NSBundle: ... - def bundleIdentifier(self) -> NSString: ... + def bundleIdentifier(self) -> str: ... NSKeyValueObservingOptions = NewType("NSKeyValueObservingOptions", int) NSKeyValueObservingOptionNew: NSKeyValueObservingOptions @@ -41,7 +60,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__.pyi b/typings/objc/__init__.pyi index ccb4348db..b0a295853 100644 --- a/typings/objc/__init__.pyi +++ b/typings/objc/__init__.pyi @@ -1,12 +1,24 @@ -from typing import Optional, TypeVar +from typing import Any, 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]: ... +def protocolNamed(name: str) -> type[Any]: ... From dcaca25a8aa8896499ceb024c8ae424cb9687102 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 14:14:55 -0500 Subject: [PATCH 081/113] backends: descriptor: remove subclassess The platform-specific subclassess were largely duplicating the same code. Rework it so that we can use the same class everywhere. --- bleak/backends/bluezdbus/client.py | 8 +-- bleak/backends/bluezdbus/descriptor.py | 51 ------------------- bleak/backends/bluezdbus/manager.py | 12 ++--- .../backends/corebluetooth/characteristic.py | 3 +- bleak/backends/corebluetooth/client.py | 31 +++++------ bleak/backends/corebluetooth/descriptor.py | 50 ------------------ bleak/backends/descriptor.py | 40 ++++++++++----- bleak/backends/p4android/client.py | 26 +++++----- bleak/backends/p4android/descriptor.py | 44 ---------------- bleak/backends/winrt/client.py | 26 +++++----- bleak/backends/winrt/descriptor.py | 42 --------------- 11 files changed, 80 insertions(+), 253 deletions(-) delete mode 100644 bleak/backends/bluezdbus/descriptor.py delete mode 100644 bleak/backends/corebluetooth/descriptor.py delete mode 100644 bleak/backends/p4android/descriptor.py delete mode 100644 bleak/backends/winrt/descriptor.py diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index dc36ad895..21c3067c7 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -788,7 +788,7 @@ async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: 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}", @@ -810,7 +810,7 @@ async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: value = bytearray(reply.body[0]) - logger.debug("Read Descriptor %s | %s: %s", handle, descriptor.path, value) + logger.debug("Read Descriptor %s | %s: %s", handle, descriptor.obj[0], value) return value @override @@ -882,7 +882,7 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: 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}", @@ -902,7 +902,7 @@ 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", handle, descriptor.obj[0], data) @override async def start_notify( diff --git a/bleak/backends/bluezdbus/descriptor.py b/bleak/backends/bluezdbus/descriptor.py deleted file mode 100644 index e991528cc..000000000 --- a/bleak/backends/bluezdbus/descriptor.py +++ /dev/null @@ -1,51 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - if sys.platform != "linux": - assert False, "This backend is only available on Linux" - -from bleak.backends.bluezdbus.defs import GattDescriptor1 -from bleak.backends.descriptor import BleakGATTDescriptor - - -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 ed4269edb..496d4844f 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -37,7 +37,6 @@ GattDescriptor1, GattService1, ) -from bleak.backends.bluezdbus.descriptor import BleakGATTDescriptorBlueZDBus from bleak.backends.bluezdbus.service import BleakGATTServiceBlueZDBus from bleak.backends.bluezdbus.signals import MatchRules, add_match from bleak.backends.bluezdbus.utils import ( @@ -45,6 +44,7 @@ device_path_from_characteristic_path, get_dbus_authenticator, ) +from bleak.backends.descriptor import BleakGATTDescriptor from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakDBusError, BleakError @@ -720,11 +720,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) diff --git a/bleak/backends/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py index a76001bb3..a9044a345 100644 --- a/bleak/backends/corebluetooth/characteristic.py +++ b/bleak/backends/corebluetooth/characteristic.py @@ -19,7 +19,6 @@ from CoreBluetooth import CBCharacteristic from bleak.backends.characteristic import BleakGATTCharacteristic -from bleak.backends.corebluetooth.descriptor import BleakGATTDescriptorCoreBluetooth from bleak.backends.corebluetooth.utils import cb_uuid_to_str from bleak.backends.descriptor import BleakGATTDescriptor @@ -71,7 +70,7 @@ 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.__descriptors: list[BleakGATTDescriptor] = [] # self.__props = obj.properties() self.__props: list[str] = [ _GattCharacteristicsPropertiesEnum[v][0] diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index a4ca283d0..fa65caf9a 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -39,11 +39,11 @@ from bleak.backends.corebluetooth.characteristic import ( BleakGATTCharacteristicCoreBluetooth, ) -from bleak.backends.corebluetooth.descriptor import BleakGATTDescriptorCoreBluetooth from bleak.backends.corebluetooth.PeripheralDelegate import PeripheralDelegate from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth from bleak.backends.corebluetooth.service import BleakGATTServiceCoreBluetooth 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 BleakGATTServiceCollection from bleak.exc import ( @@ -247,24 +247,25 @@ async def _get_services(self) -> BleakGATTServiceCollection: 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 = BleakGATTCharacteristicCoreBluetooth( + characteristic, + lambda: self._peripheral.maximumWriteValueLengthForType_( + CBCharacteristicWriteWithoutResponse + ), ) + 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 diff --git a/bleak/backends/corebluetooth/descriptor.py b/bleak/backends/corebluetooth/descriptor.py deleted file mode 100644 index 1521d968e..000000000 --- a/bleak/backends/corebluetooth/descriptor.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Interface class for the Bleak representation of a GATT Descriptor - -Created on 2019-06-28 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" - -from CoreBluetooth import CBDescriptor - -from bleak.backends.corebluetooth.utils import cb_uuid_to_str -from bleak.backends.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/descriptor.py b/bleak/backends/descriptor.py index f79cd3000..bed6dcf8d 100644 --- a/bleak/backends/descriptor.py +++ b/bleak/backends/descriptor.py @@ -5,11 +5,17 @@ Created on 2019-03-19 by hbldh """ -import abc -from typing import Any +from __future__ import annotations + +from typing import TYPE_CHECKING, Any from bleak.uuids import normalize_uuid_16 +# avoid circular import +if TYPE_CHECKING: + from bleak.backends.characteristic import BleakGATTCharacteristic + + _descriptor_descriptions = { normalize_uuid_16(0x2905): [ "Characteristic Aggregate Format", @@ -104,38 +110,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/p4android/client.py b/bleak/backends/p4android/client.py index 61d1b9fc1..b1e909f65 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -25,10 +25,10 @@ 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.p4android.characteristic import BleakGATTCharacteristicP4Android -from bleak.backends.p4android.descriptor import BleakGATTDescriptorP4Android from bleak.backends.p4android.service import BleakGATTServiceP4Android from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakCharacteristicNotFoundError, BleakError @@ -273,11 +273,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) @@ -325,21 +325,21 @@ async def read_gatt_char( @override async def read_gatt_descriptor( self, - desc_specifier: Union[BleakGATTDescriptorP4Android, str, uuid.UUID], + desc_specifier: Union[BleakGATTDescriptor, str, uuid.UUID], **kwargs, ) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: - desc_specifier (BleakGATTDescriptorP4Android, str or UUID): The descriptor to read from, + desc_specifier (BleakGATTDescriptor, str or UUID): The descriptor to read from, specified by either UUID or directly by the - BleakGATTDescriptorP4Android object representing it. + BleakGATTDescriptor object representing it. Returns: (bytearray) The read data. """ - if not isinstance(desc_specifier, BleakGATTDescriptorP4Android): + if not isinstance(desc_specifier, BleakGATTDescriptor): descriptor = self.services.get_descriptor(desc_specifier) else: descriptor = desc_specifier @@ -391,19 +391,19 @@ async def write_gatt_char( @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 diff --git a/bleak/backends/p4android/descriptor.py b/bleak/backends/p4android/descriptor.py deleted file mode 100644 index 1ed8a3e14..000000000 --- a/bleak/backends/p4android/descriptor.py +++ /dev/null @@ -1,44 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - if sys.platform != "android": - assert False, "This backend is only available on Android" - -from bleak.backends.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/winrt/client.py b/bleak/backends/winrt/client.py index e78951d05..3249a738d 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -5,8 +5,6 @@ Created on 2020-08-19 by hbldh """ import sys -from collections.abc import Callable -from contextvars import Context from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -16,6 +14,8 @@ import asyncio import logging import uuid +from collections.abc import Callable +from contextvars import Context from ctypes import WinError from typing import Any, Generic, Optional, Protocol, Sequence, TypeVar, Union, cast @@ -72,10 +72,10 @@ from bleak.args.winrt import WinRTClientArgs 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.backends.winrt.characteristic import BleakGATTCharacteristicWinRT -from bleak.backends.winrt.descriptor import BleakGATTDescriptorWinRT from bleak.backends.winrt.scanner import BleakScannerWinRT, RawAdvData from bleak.backends.winrt.service import BleakGATTServiceWinRT from bleak.exc import ( @@ -731,20 +731,20 @@ async def _get_services( 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 = BleakGATTCharacteristicWinRT( + characteristic, lambda: self._session.max_pdu_size - 3 ) + 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: diff --git a/bleak/backends/winrt/descriptor.py b/bleak/backends/winrt/descriptor.py deleted file mode 100644 index fb2c7c706..000000000 --- a/bleak/backends/winrt/descriptor.py +++ /dev/null @@ -1,42 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - if sys.platform != "win32": - assert False, "This backend is only available on Windows" - -from winrt.windows.devices.bluetooth.genericattributeprofile import GattDescriptor - -from bleak.backends.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 From a99afb6107a6d1dd79d1f1f53a10b3c84d65b1aa Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 19:30:22 -0500 Subject: [PATCH 082/113] backends: characteristic: remove subclasses The BleakGATTCharacteristic were largely duplicating code and can be consolidated to a single class shared by all backends. --- bleak/assigned_numbers.py | 55 +++++++ bleak/backends/bluezdbus/characteristic.py | 95 ------------ bleak/backends/bluezdbus/client.py | 37 +++-- bleak/backends/bluezdbus/defs.py | 26 +--- bleak/backends/bluezdbus/manager.py | 14 +- bleak/backends/bluezdbus/service.py | 12 +- bleak/backends/characteristic.py | 69 ++++++--- .../backends/corebluetooth/characteristic.py | 128 ---------------- bleak/backends/corebluetooth/client.py | 15 +- bleak/backends/corebluetooth/service.py | 14 +- bleak/backends/p4android/characteristic.py | 104 ------------- bleak/backends/p4android/client.py | 26 ++-- bleak/backends/p4android/defs.py | 11 -- bleak/backends/p4android/service.py | 8 +- bleak/backends/winrt/characteristic.py | 142 ------------------ bleak/backends/winrt/client.py | 18 ++- bleak/backends/winrt/service.py | 10 +- 17 files changed, 187 insertions(+), 597 deletions(-) delete mode 100644 bleak/backends/bluezdbus/characteristic.py delete mode 100644 bleak/backends/corebluetooth/characteristic.py delete mode 100644 bleak/backends/p4android/characteristic.py delete mode 100644 bleak/backends/winrt/characteristic.py 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/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py deleted file mode 100644 index 12ed5c961..000000000 --- a/bleak/backends/bluezdbus/characteristic.py +++ /dev/null @@ -1,95 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - if sys.platform != "linux": - assert False, "This backend is only available on Linux" - -from collections.abc import Callable -from typing import Union -from uuid import UUID - -from bleak.backends.bluezdbus.defs import GattCharacteristic1 -from bleak.backends.bluezdbus.utils import extract_service_handle_from_path -from bleak.backends.characteristic import BleakGATTCharacteristic -from bleak.backends.descriptor import BleakGATTDescriptor - - -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: list[BleakGATTDescriptor] = [] - 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 21c3067c7..5f7c98cdb 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -36,7 +36,6 @@ from bleak import BleakScanner from bleak.backends.bluezdbus import defs -from bleak.backends.bluezdbus.characteristic import BleakGATTCharacteristicBlueZDBus 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 @@ -603,7 +602,7 @@ async def _acquire_mtu(self) -> None: 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}", @@ -693,15 +692,15 @@ async def _get_services( @override async def read_gatt_char( self, - char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID], + char_specifier: Union[BleakGATTCharacteristic, int, str, UUID], **kwargs: Any, ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to read from, + char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from, specified by either integer handle, UUID or directly by the - BleakGATTCharacteristicBlueZDBus object representing it. + BleakGATTCharacteristic object representing it. Returns: (bytearray) The read data. @@ -710,7 +709,7 @@ async def read_gatt_char( if not self.is_connected: raise BleakError("Not connected") - if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus): + if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier @@ -735,7 +734,7 @@ async def read_gatt_char( 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}", @@ -759,7 +758,7 @@ 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 @@ -829,7 +828,7 @@ async def write_gatt_char( reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, - path=characteristic.path, + path=characteristic.obj[0], interface=defs.GATT_CHARACTERISTIC_INTERFACE, member="WriteValue", signature="aya{sv}", @@ -855,7 +854,7 @@ async def write_gatt_char( logger.debug( "Write Characteristic %s | %s: %s", characteristic.uuid, - characteristic.path, + characteristic.obj[0], data, ) @@ -914,16 +913,16 @@ async def start_notify( """ Activate notifications/indications on a characteristic. """ - characteristic = cast(BleakGATTCharacteristicBlueZDBus, characteristic) + characteristic = cast(BleakGATTCharacteristic, 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", ) @@ -934,20 +933,20 @@ async def start_notify( @override async def stop_notify( self, - char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID], + char_specifier: Union[BleakGATTCharacteristic, int, str, UUID], ) -> None: """Deactivate notification/indication on a specified characteristic. Args: - char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to deactivate + char_specifier (BleakGATTCharacteristic, 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. + directly by the BleakGATTCharacteristic object representing it. """ if not self.is_connected: raise BleakError("Not connected") - if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus): + if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier @@ -957,7 +956,7 @@ async def stop_notify( reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, - path=characteristic.path, + path=characteristic.obj[0], interface=defs.GATT_CHARACTERISTIC_INTERFACE, member="StopNotify", ) @@ -965,4 +964,4 @@ async def stop_notify( 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 ee50d7694..6b615fb52 100644 --- a/bleak/backends/bluezdbus/defs.py +++ b/bleak/backends/bluezdbus/defs.py @@ -2,6 +2,8 @@ from typing import Literal, TypedDict +from bleak.assigned_numbers import CharacteristicPropertyName + # DBus Interfaces OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager" PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties" @@ -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 diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index 496d4844f..ff0cd4f81 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -30,7 +30,6 @@ AdvertisementMonitor, OrPatternLike, ) -from bleak.backends.bluezdbus.characteristic import BleakGATTCharacteristicBlueZDBus from bleak.backends.bluezdbus.defs import ( Device1, GattCharacteristic1, @@ -42,8 +41,10 @@ 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 BleakGATTServiceCollection from bleak.exc import BleakDBusError, BleakError @@ -702,14 +703,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) diff --git a/bleak/backends/bluezdbus/service.py b/bleak/backends/bluezdbus/service.py index 5647a1bf4..3a7a064a2 100644 --- a/bleak/backends/bluezdbus/service.py +++ b/bleak/backends/bluezdbus/service.py @@ -7,8 +7,8 @@ from typing import Any -from bleak.backends.bluezdbus.characteristic import BleakGATTCharacteristicBlueZDBus from bleak.backends.bluezdbus.utils import extract_service_handle_from_path +from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.service import BleakGATTService @@ -17,7 +17,7 @@ class BleakGATTServiceBlueZDBus(BleakGATTService): def __init__(self, obj: Any, path: str): super().__init__(obj) - self.__characteristics: list[BleakGATTCharacteristicBlueZDBus] = [] + self.__characteristics: list[BleakGATTCharacteristic] = [] self.__path = path self.__handle = extract_service_handle_from_path(path) @@ -32,14 +32,12 @@ def handle(self) -> int: return self.__handle @property - def characteristics(self) -> list[BleakGATTCharacteristicBlueZDBus]: + def characteristics(self) -> list[BleakGATTCharacteristic]: """List of characteristics for this service""" return self.__characteristics - def add_characteristic( - self, characteristic: BleakGATTCharacteristicBlueZDBus - ) -> None: - """Add a :py:class:`~BleakGATTCharacteristicBlueZDBus` to the service. + 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. """ diff --git a/bleak/backends/characteristic.py b/bleak/backends/characteristic.py index 2f47638c9..c01eecf4f 100644 --- a/bleak/backends/characteristic.py +++ b/bleak/backends/characteristic.py @@ -5,14 +5,19 @@ Created on 2019-03-19 by hbldh """ -import abc +from __future__ import annotations + import enum from collections.abc import Callable -from typing import Any, Union +from typing import TYPE_CHECKING, Any, Union from uuid import UUID from bleak.backends.descriptor import BleakGATTDescriptor -from bleak.uuids import uuidstr_to_str +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): @@ -28,10 +33,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[str], + max_write_without_response_size: Callable[[], int], + service: BleakGATTService, + ): """ Args: obj: @@ -39,36 +52,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: @@ -76,10 +92,9 @@ def description(self) -> str: return uuidstr_to_str(self.uuid) @property - @abc.abstractmethod def properties(self) -> list[str]: """Properties of this characteristic""" - raise NotImplementedError() + return self._properties @property def max_write_without_response_size(self) -> int: @@ -113,22 +128,32 @@ def max_write_without_response_size(self) -> int: return self._max_write_without_response_size() @property - @abc.abstractmethod 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/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py deleted file mode 100644 index a9044a345..000000000 --- a/bleak/backends/corebluetooth/characteristic.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -Interface class for the Bleak representation of a GATT Characteristic - -Created on 2019-06-28 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" - -from collections.abc import Callable -from enum import Enum -from typing import Optional, Union - -from CoreBluetooth import CBCharacteristic - -from bleak.backends.characteristic import BleakGATTCharacteristic -from bleak.backends.corebluetooth.utils import cb_uuid_to_str -from bleak.backends.descriptor import BleakGATTDescriptor - - -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[BleakGATTDescriptor] = [] - # 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 fa65caf9a..e73509455 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -33,12 +33,10 @@ 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.characteristic import ( - BleakGATTCharacteristicCoreBluetooth, -) from bleak.backends.corebluetooth.PeripheralDelegate import PeripheralDelegate from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth from bleak.backends.corebluetooth.service import BleakGATTServiceCoreBluetooth @@ -234,25 +232,30 @@ async def _get_services(self) -> BleakGATTServiceCollection: cb_services = await self._delegate.discover_services(self._requested_services) for service in cb_services: + serv = BleakGATTServiceCoreBluetooth(service) + 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) ) - char = BleakGATTCharacteristicCoreBluetooth( + 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) diff --git a/bleak/backends/corebluetooth/service.py b/bleak/backends/corebluetooth/service.py index 06d40d5a0..dba3a1d58 100644 --- a/bleak/backends/corebluetooth/service.py +++ b/bleak/backends/corebluetooth/service.py @@ -7,9 +7,7 @@ from CoreBluetooth import CBService -from bleak.backends.corebluetooth.characteristic import ( - BleakGATTCharacteristicCoreBluetooth, -) +from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.corebluetooth.utils import cb_uuid_to_str from bleak.backends.service import BleakGATTService @@ -19,7 +17,7 @@ class BleakGATTServiceCoreBluetooth(BleakGATTService): def __init__(self, obj: CBService): super().__init__(obj) - self.__characteristics: list[BleakGATTCharacteristicCoreBluetooth] = [] + self.__characteristics: list[BleakGATTCharacteristic] = [] # 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()) @@ -35,14 +33,12 @@ def uuid(self) -> str: return cb_uuid_to_str(self.obj.UUID()) @property - def characteristics(self) -> list[BleakGATTCharacteristicCoreBluetooth]: + def characteristics(self) -> list[BleakGATTCharacteristic]: """List of characteristics for this service""" return self.__characteristics - def add_characteristic( - self, characteristic: BleakGATTCharacteristicCoreBluetooth - ) -> None: - """Add a :py:class:`~BleakGATTCharacteristicCoreBluetooth` to the service. + 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. """ diff --git a/bleak/backends/p4android/characteristic.py b/bleak/backends/p4android/characteristic.py deleted file mode 100644 index 3edac49a9..000000000 --- a/bleak/backends/p4android/characteristic.py +++ /dev/null @@ -1,104 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - if sys.platform != "android": - assert False, "This backend is only available on Android" - -from collections.abc import Callable -from typing import Union -from uuid import UUID - -from bleak.backends.characteristic import BleakGATTCharacteristic -from bleak.backends.descriptor import BleakGATTDescriptor -from bleak.backends.p4android import defs -from bleak.exc import BleakError - - -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 b1e909f65..fbe59a5df 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -23,12 +23,12 @@ from android.broadcast import BroadcastReceiver from jnius import java_method +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.p4android.characteristic import BleakGATTCharacteristicP4Android from bleak.backends.p4android.service import BleakGATTServiceP4Android from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakCharacteristicNotFoundError, BleakError @@ -261,11 +261,13 @@ async def _get_services(self) -> BleakGATTServiceCollection: 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) @@ -289,21 +291,21 @@ async def _get_services(self) -> BleakGATTServiceCollection: @override async def read_gatt_char( self, - char_specifier: Union[BleakGATTCharacteristicP4Android, int, str, uuid.UUID], + char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], **kwargs, ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to read from, + char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from, specified by either integer handle, UUID or directly by the - BleakGATTCharacteristicP4Android object representing it. + BleakGATTCharacteristic object representing it. Returns: (bytearray) The read data. """ - if not isinstance(char_specifier, BleakGATTCharacteristicP4Android): + if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier @@ -450,17 +452,17 @@ async def start_notify( @override async def stop_notify( self, - char_specifier: Union[BleakGATTCharacteristicP4Android, int, str, uuid.UUID], + char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], ) -> None: """Deactivate notification/indication on a specified characteristic. Args: - char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to deactivate + char_specifier (BleakGATTCharacteristic, 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. + directly by the BleakGATTCharacteristic object representing it. """ - if not isinstance(char_specifier, BleakGATTCharacteristicP4Android): + if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier diff --git a/bleak/backends/p4android/defs.py b/bleak/backends/p4android/defs.py index 50cc47fbd..c5a408251 100644 --- a/bleak/backends/p4android/defs.py +++ b/bleak/backends/p4android/defs.py @@ -82,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/service.py b/bleak/backends/p4android/service.py index 68991dc0a..83ffa5e91 100644 --- a/bleak/backends/p4android/service.py +++ b/bleak/backends/p4android/service.py @@ -5,7 +5,7 @@ if sys.platform != "android": assert False, "This backend is only available on Android" -from bleak.backends.p4android.characteristic import BleakGATTCharacteristicP4Android +from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.service import BleakGATTService @@ -29,12 +29,12 @@ def handle(self) -> int: return self.__handle @property - def characteristics(self) -> list[BleakGATTCharacteristicP4Android]: + def characteristics(self) -> list[BleakGATTCharacteristic]: """List of characteristics for this service""" return self.__characteristics - def add_characteristic(self, characteristic: BleakGATTCharacteristicP4Android): - """Add a :py:class:`~BleakGATTCharacteristicP4Android` to the service. + def add_characteristic(self, characteristic: BleakGATTCharacteristic): + """Add a :py:class:`~BleakGATTCharacteristic` to the service. Should not be used by end user, but rather by `bleak` itself. """ diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py deleted file mode 100644 index 0d89d3c94..000000000 --- a/bleak/backends/winrt/characteristic.py +++ /dev/null @@ -1,142 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - if sys.platform != "win32": - assert False, "This backend is only available on Windows" - -from collections.abc import Callable -from typing import Union -from uuid import UUID - -from winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattCharacteristic, - GattCharacteristicProperties, -) - -from bleak.backends.characteristic import BleakGATTCharacteristic -from bleak.backends.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) -> 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) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 3249a738d..4e0ef874d 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -70,12 +70,12 @@ from bleak import BleakScanner from bleak.args.winrt import 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 BleakGATTServiceCollection -from bleak.backends.winrt.characteristic import BleakGATTCharacteristicWinRT from bleak.backends.winrt.scanner import BleakScannerWinRT, RawAdvData from bleak.backends.winrt.service import BleakGATTServiceWinRT from bleak.exc import ( @@ -711,7 +711,8 @@ async def _get_services( f"Could not get GATT characteristics for service {service.uuid} ({service.attribute_handle})", ) - new_services.add_service(BleakGATTServiceWinRT(service)) + serv = BleakGATTServiceWinRT(service) + new_services.add_service(serv) for characteristic in characteristics: if cache_mode is not None: @@ -731,8 +732,17 @@ async def _get_services( f"Could not get GATT descriptors for characteristic {characteristic.uuid} ({characteristic.attribute_handle})", ) - char = 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) diff --git a/bleak/backends/winrt/service.py b/bleak/backends/winrt/service.py index 6e18b4e77..2a3fcf541 100644 --- a/bleak/backends/winrt/service.py +++ b/bleak/backends/winrt/service.py @@ -7,8 +7,8 @@ from winrt.windows.devices.bluetooth.genericattributeprofile import GattDeviceService +from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.service import BleakGATTService -from bleak.backends.winrt.characteristic import BleakGATTCharacteristicWinRT class BleakGATTServiceWinRT(BleakGATTService): @@ -16,7 +16,7 @@ class BleakGATTServiceWinRT(BleakGATTService): def __init__(self, obj: GattDeviceService) -> None: super().__init__(obj) - self.__characteristics: list[BleakGATTCharacteristicWinRT] = [] + self.__characteristics: list[BleakGATTCharacteristic] = [] @property def uuid(self) -> str: @@ -27,12 +27,12 @@ def handle(self) -> int: return self.obj.attribute_handle @property - def characteristics(self) -> list[BleakGATTCharacteristicWinRT]: + def characteristics(self) -> list[BleakGATTCharacteristic]: """List of characteristics for this service""" return self.__characteristics - def add_characteristic(self, characteristic: BleakGATTCharacteristicWinRT) -> None: - """Add a :py:class:`~BleakGATTCharacteristicWinRT` to the service. + 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. """ From 38941615e55b71de5145f7f612ed3b6ca2290514 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 20:20:06 -0500 Subject: [PATCH 083/113] backends: service: remove BleakGATTService subclasses The BleakGATTService subclasses were just duplicating code and can be removed. --- bleak/backends/bluezdbus/manager.py | 9 +++-- bleak/backends/bluezdbus/service.py | 49 ------------------------- bleak/backends/corebluetooth/client.py | 7 ++-- bleak/backends/corebluetooth/service.py | 45 ----------------------- bleak/backends/p4android/client.py | 9 +++-- bleak/backends/p4android/service.py | 41 --------------------- bleak/backends/service.py | 32 +++++++++------- bleak/backends/winrt/client.py | 7 ++-- bleak/backends/winrt/service.py | 39 -------------------- 9 files changed, 39 insertions(+), 199 deletions(-) delete mode 100644 bleak/backends/bluezdbus/service.py delete mode 100644 bleak/backends/corebluetooth/service.py delete mode 100644 bleak/backends/p4android/service.py delete mode 100644 bleak/backends/winrt/service.py diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index ff0cd4f81..485cef849 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -36,7 +36,6 @@ GattDescriptor1, GattService1, ) -from bleak.backends.bluezdbus.service import BleakGATTServiceBlueZDBus from bleak.backends.bluezdbus.signals import MatchRules, add_match from bleak.backends.bluezdbus.utils import ( assert_reply, @@ -46,7 +45,7 @@ ) from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.descriptor import BleakGATTDescriptor -from bleak.backends.service import BleakGATTServiceCollection +from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection from bleak.exc import BleakDBusError, BleakError logger = logging.getLogger(__name__) @@ -687,7 +686,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 diff --git a/bleak/backends/bluezdbus/service.py b/bleak/backends/bluezdbus/service.py deleted file mode 100644 index 3a7a064a2..000000000 --- a/bleak/backends/bluezdbus/service.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - if sys.platform != "linux": - assert False, "This backend is only available on Linux" - -from typing import Any - -from bleak.backends.bluezdbus.utils import extract_service_handle_from_path -from bleak.backends.characteristic import BleakGATTCharacteristic -from bleak.backends.service import BleakGATTService - - -class BleakGATTServiceBlueZDBus(BleakGATTService): - """GATT Service implementation for the BlueZ DBus backend""" - - def __init__(self, obj: Any, path: str): - super().__init__(obj) - self.__characteristics: list[BleakGATTCharacteristic] = [] - 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[BleakGATTCharacteristic]: - """List of characteristics for this service""" - return self.__characteristics - - 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. - """ - 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/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index e73509455..1543c5113 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -39,11 +39,10 @@ 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.service import BleakGATTServiceCoreBluetooth 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 BleakGATTServiceCollection +from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection from bleak.exc import ( BleakCharacteristicNotFoundError, BleakDeviceNotFoundError, @@ -232,7 +231,9 @@ async def _get_services(self) -> BleakGATTServiceCollection: cb_services = await self._delegate.discover_services(self._requested_services) for service in cb_services: - serv = BleakGATTServiceCoreBluetooth(service) + serv = BleakGATTService( + service, service.startHandle(), cb_uuid_to_str(service.UUID()) + ) services.add_service(serv) serviceUUID = service.UUID().UUIDString() diff --git a/bleak/backends/corebluetooth/service.py b/bleak/backends/corebluetooth/service.py deleted file mode 100644 index dba3a1d58..000000000 --- a/bleak/backends/corebluetooth/service.py +++ /dev/null @@ -1,45 +0,0 @@ -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 CBService - -from bleak.backends.characteristic import BleakGATTCharacteristic -from bleak.backends.corebluetooth.utils import cb_uuid_to_str -from bleak.backends.service import BleakGATTService - - -class BleakGATTServiceCoreBluetooth(BleakGATTService): - """GATT Characteristic implementation for the CoreBluetooth backend""" - - def __init__(self, obj: CBService): - super().__init__(obj) - self.__characteristics: list[BleakGATTCharacteristic] = [] - # 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[BleakGATTCharacteristic]: - """List of characteristics for this service""" - return self.__characteristics - - 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. - """ - self.__characteristics.append(characteristic) diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index fbe59a5df..60a1cd6c9 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -29,8 +29,7 @@ from bleak.backends.descriptor import BleakGATTDescriptor from bleak.backends.device import BLEDevice from bleak.backends.p4android import defs, utils -from bleak.backends.p4android.service import BleakGATTServiceP4Android -from bleak.backends.service import BleakGATTServiceCollection +from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection from bleak.exc import BleakCharacteristicNotFoundError, BleakError logger = logging.getLogger(__name__) @@ -256,7 +255,11 @@ 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(): diff --git a/bleak/backends/p4android/service.py b/bleak/backends/p4android/service.py deleted file mode 100644 index 83ffa5e91..000000000 --- a/bleak/backends/p4android/service.py +++ /dev/null @@ -1,41 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - if sys.platform != "android": - assert False, "This backend is only available on Android" - -from bleak.backends.characteristic import BleakGATTCharacteristic -from bleak.backends.service import BleakGATTService - - -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[BleakGATTCharacteristic]: - """List of characteristics for this service""" - return self.__characteristics - - def add_characteristic(self, characteristic: BleakGATTCharacteristic): - """Add a :py:class:`~BleakGATTCharacteristic` to the service. - - Should not be used by end user, but rather by `bleak` itself. - """ - self.__characteristics.append(characteristic) diff --git a/bleak/backends/service.py b/bleak/backends/service.py index ff07329e1..f8d85a199 100644 --- a/bleak/backends/service.py +++ b/bleak/backends/service.py @@ -5,7 +5,6 @@ Created on 2019-03-19 by hbldh """ -import abc import logging from collections.abc import Iterator from typing import Any, Optional, Union @@ -19,26 +18,27 @@ 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: @@ -46,18 +46,22 @@ def description(self) -> str: return uuidstr_to_str(self.uuid) @property - @abc.abstractmethod 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] @@ -74,7 +78,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 diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 4e0ef874d..532957a38 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -75,9 +75,8 @@ 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.backends.service import BleakGATTService, BleakGATTServiceCollection from bleak.backends.winrt.scanner import BleakScannerWinRT, RawAdvData -from bleak.backends.winrt.service import BleakGATTServiceWinRT from bleak.exc import ( PROTOCOL_ERROR_CODES, BleakCharacteristicNotFoundError, @@ -711,7 +710,9 @@ async def _get_services( f"Could not get GATT characteristics for service {service.uuid} ({service.attribute_handle})", ) - serv = BleakGATTServiceWinRT(service) + serv = BleakGATTService( + service, service.attribute_handle, str(service.uuid) + ) new_services.add_service(serv) for characteristic in characteristics: diff --git a/bleak/backends/winrt/service.py b/bleak/backends/winrt/service.py deleted file mode 100644 index 2a3fcf541..000000000 --- a/bleak/backends/winrt/service.py +++ /dev/null @@ -1,39 +0,0 @@ -import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - if sys.platform != "win32": - assert False, "This backend is only available on Windows" - -from winrt.windows.devices.bluetooth.genericattributeprofile import GattDeviceService - -from bleak.backends.characteristic import BleakGATTCharacteristic -from bleak.backends.service import BleakGATTService - - -class BleakGATTServiceWinRT(BleakGATTService): - """GATT Characteristic implementation for the .NET backend, implemented with WinRT""" - - def __init__(self, obj: GattDeviceService) -> None: - super().__init__(obj) - self.__characteristics: list[BleakGATTCharacteristic] = [] - - @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[BleakGATTCharacteristic]: - """List of characteristics for this service""" - return self.__characteristics - - 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. - """ - self.__characteristics.append(characteristic) From 15945c898d9a544dfe22e40a5f800d24238fd1c3 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 21:17:04 -0500 Subject: [PATCH 084/113] backends: refactor common characteristics lookup code The code for looking up a characteristic by handle or UUID was duplicated many times. This also removes a fallback to fake reading the device name characteristic on the BlueZ backend. We'll see if anyone complains about that and can replace it with a new device name property. --- bleak/__init__.py | 56 +++++++++++++----- bleak/backends/bluezdbus/client.py | 58 +++---------------- bleak/backends/client.py | 23 ++------ .../corebluetooth/PeripheralDelegate.py | 2 +- bleak/backends/corebluetooth/client.py | 50 +++------------- bleak/backends/p4android/client.py | 41 +++---------- bleak/backends/winrt/client.py | 44 +++----------- 7 files changed, 77 insertions(+), 197 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index acca004cc..1a4392eae 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -376,6 +376,22 @@ async def find_device_by_filter( 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 + + class BleakClient: """The Client interface for connecting to a specific BLE GATT server and communicating with it. @@ -611,8 +627,13 @@ 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, @@ -655,6 +676,11 @@ async def write_gatt_char( 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. @@ -664,13 +690,7 @@ 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- @@ -715,6 +735,10 @@ def callback(sender: BleakGATTCharacteristic, data: bytearray): 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` @@ -725,13 +749,7 @@ def callback(sender: BleakGATTCharacteristic, data: bytearray): 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): @@ -759,11 +777,17 @@ 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: Any) -> bytearray: """ diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 5f7c98cdb..42e6c0d33 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -16,7 +16,6 @@ from collections.abc import Callable from contextlib import AsyncExitStack from typing import Any, Optional, Union, cast -from uuid import UUID if sys.version_info < (3, 12): from typing_extensions import Buffer, override @@ -44,12 +43,7 @@ from bleak.backends.client import BaseBleakClient, NotifyCallback from bleak.backends.device import BLEDevice from bleak.backends.service import BleakGATTServiceCollection -from bleak.exc import ( - BleakCharacteristicNotFoundError, - BleakDBusError, - BleakDeviceNotFoundError, - BleakError, -) +from bleak.exc import BleakDBusError, BleakDeviceNotFoundError, BleakError logger = logging.getLogger(__name__) @@ -691,16 +685,12 @@ async def _get_services( @override async def read_gatt_char( - self, - char_specifier: Union[BleakGATTCharacteristic, int, str, UUID], - **kwargs: Any, + 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. @@ -709,25 +699,6 @@ async def read_gatt_char( 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: - if str(char_specifier) == "00002a00-0000-1000-8000-00805f9b34fb": - # 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 @@ -814,10 +785,7 @@ async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: @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") @@ -931,28 +899,16 @@ async def start_notify( assert_reply(reply) @override - async def stop_notify( - self, - char_specifier: Union[BleakGATTCharacteristic, int, str, 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. """ 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) - reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, diff --git a/bleak/backends/client.py b/bleak/backends/client.py index 5c9cbcada..a8d421de5 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -9,7 +9,6 @@ import os import platform import sys -import uuid from collections.abc import Callable from typing import Any, Optional, Union @@ -119,16 +118,12 @@ def is_connected(self) -> bool: @abc.abstractmethod async def read_gatt_char( - self, - char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - **kwargs: Any, + 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. @@ -151,10 +146,7 @@ async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: @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. @@ -196,15 +188,12 @@ 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() diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index 5ba22297e..825dfed64 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -166,7 +166,7 @@ async def discover_descriptors( async def read_characteristic( self, characteristic: CBCharacteristic, - use_cached: bool = True, + use_cached: bool, timeout: int = 20, ) -> NSData: value = characteristic.value() diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 1543c5113..07d8f4005 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -13,7 +13,6 @@ import asyncio import logging -import uuid from typing import Optional, Union if sys.version_info < (3, 12): @@ -43,11 +42,7 @@ from bleak.backends.descriptor import BleakGATTDescriptor from bleak.backends.device import BLEDevice from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection -from bleak.exc import ( - BleakCharacteristicNotFoundError, - BleakDeviceNotFoundError, - BleakError, -) +from bleak.exc import BleakDeviceNotFoundError, BleakError logger = logging.getLogger(__name__) @@ -276,34 +271,20 @@ async def _get_services(self) -> BleakGATTServiceCollection: @override async def read_gatt_char( - self, - char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - use_cached: bool = False, - **kwargs: Any, + 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)) @@ -342,10 +323,7 @@ async def read_gatt_descriptor( @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( @@ -398,25 +376,13 @@ async def start_notify( ) @override - 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. """ - 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) diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index 60a1cd6c9..4edbf176e 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -13,7 +13,7 @@ import logging import uuid import warnings -from typing import Optional, Union +from typing import Any, Optional, Union if sys.version_info < (3, 12): from typing_extensions import override @@ -30,7 +30,7 @@ from bleak.backends.device import BLEDevice from bleak.backends.p4android import defs, utils from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection -from bleak.exc import BleakCharacteristicNotFoundError, BleakError +from bleak.exc import BleakError logger = logging.getLogger(__name__) @@ -293,28 +293,17 @@ async def _get_services(self) -> BleakGATTServiceCollection: @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. 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) (value,) = await self.__callbacks.perform_and_wait( dispatchApi=self.__gatt.readCharacteristic, @@ -367,10 +356,7 @@ async def read_gatt_descriptor( @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( @@ -453,25 +439,14 @@ async def start_notify( ) @override - 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,. """ - if not isinstance(char_specifier, BleakGATTCharacteristic): - 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/winrt/client.py b/bleak/backends/winrt/client.py index 532957a38..1319b14ea 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -77,12 +77,7 @@ 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, - BleakCharacteristicNotFoundError, - BleakDeviceNotFoundError, - BleakError, -) +from bleak.exc import PROTOCOL_ERROR_CODES, BleakDeviceNotFoundError, BleakError logger = logging.getLogger(__name__) @@ -776,16 +771,12 @@ async def _get_services( @override async def read_gatt_char( - self, - char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - **kwargs: Any, + 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 @@ -802,13 +793,6 @@ async def read_gatt_char( 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( @@ -873,10 +857,7 @@ async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: @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") @@ -998,29 +979,18 @@ def handle_value_changed( raise @override - 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. """ if not self.is_connected: raise BleakError("Not connected") assert self.services - 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) _ensure_success( From 8d832d32a963f66df25db8ffcda6bd21e19ebf21 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 21:50:21 -0500 Subject: [PATCH 085/113] backends: refactor resolving descriptors The public API said that it only accepted int handles but some of the backends actually would accept a BleakGATTDescriptor object. Now this is made public and common code is refactored to resolve the handle to the descriptor object. --- bleak/__init__.py | 52 ++++++++++++++++++++++---- bleak/backends/bluezdbus/client.py | 31 ++++++++------- bleak/backends/client.py | 15 +++++--- bleak/backends/corebluetooth/client.py | 22 ++++------- bleak/backends/p4android/client.py | 19 ++-------- bleak/backends/winrt/client.py | 31 +++++++-------- 6 files changed, 94 insertions(+), 76 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 1a4392eae..dbf1fbaee 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -35,6 +35,7 @@ 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, @@ -392,6 +393,22 @@ def _resolve_characteristic( 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. @@ -789,31 +806,52 @@ async def stop_notify( characteristic = _resolve_characteristic(char_specifier, self.services) await self._backend.stop_notify(characteristic) - async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> 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. + Raises: + BleakError: if the descriptor could not be found. + backend-specific exceptions: if the read operation failed. + """ - await self._backend.write_gatt_descriptor(handle, data) + descriptor = _resolve_descriptor(desc_specifier, self.services) + await self._backend.write_gatt_descriptor(descriptor, data) def cli() -> None: diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 42e6c0d33..68c0e724b 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -41,6 +41,7 @@ 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 @@ -735,23 +736,20 @@ async def read_gatt_char( return value @override - async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> 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. """ 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 @@ -780,7 +778,9 @@ async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: value = bytearray(reply.body[0]) - logger.debug("Read Descriptor %s | %s: %s", handle, descriptor.obj[0], value) + logger.debug( + "Read Descriptor %s | %s: %s", descriptor.handle, descriptor.obj[0], value + ) return value @override @@ -827,7 +827,9 @@ async def write_gatt_char( ) @override - 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: @@ -838,11 +840,6 @@ 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 @@ -869,7 +866,9 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: assert_reply(reply) break - logger.debug("Write Descriptor %s | %s: %s", handle, descriptor.obj[0], data) + logger.debug( + "Write Descriptor %s | %s: %s", descriptor.handle, descriptor.obj[0], data + ) @override async def start_notify( diff --git a/bleak/backends/client.py b/bleak/backends/client.py index a8d421de5..7b0dd8d33 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -18,6 +18,7 @@ from collections.abc import Buffer 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 @@ -132,14 +133,16 @@ async def read_gatt_char( raise NotImplementedError() @abc.abstractmethod - async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> 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() @@ -159,11 +162,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). """ diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 07d8f4005..0589c7a16 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -292,7 +292,7 @@ async def read_gatt_char( @override async def read_gatt_descriptor( - self, handle: int, use_cached: bool = False, **kwargs: Any + self, descriptor: BleakGATTDescriptor, **kwargs: Any ) -> bytearray: """Perform read operation on the specified GATT descriptor. @@ -304,13 +304,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 @@ -318,7 +314,7 @@ 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 @@ -338,22 +334,20 @@ async def write_gatt_char( logger.debug(f"Write Characteristic {characteristic.uuid} : {data}") @override - 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). """ - 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( diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index 4edbf176e..efa13f5ae 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -318,29 +318,16 @@ async def read_gatt_char( @override async def read_gatt_descriptor( - self, - desc_specifier: Union[BleakGATTDescriptor, str, uuid.UUID], - **kwargs, + self, descriptor: BleakGATTDescriptor, **kwargs: Any ) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: - desc_specifier (BleakGATTDescriptor, str or UUID): The descriptor to read from, - specified by either UUID or directly by the - BleakGATTDescriptor object representing it. + descriptor: The descriptor to read from. Returns: - (bytearray) The read data. - + The read data. """ - if not isinstance(desc_specifier, BleakGATTDescriptor): - 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,), diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 1319b14ea..1b48cb314 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -812,18 +812,20 @@ async def read_gatt_char( return value @override - async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> 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. 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: @@ -832,11 +834,6 @@ async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: assert self.services use_cached = kwargs.get("use_cached", False) - - descriptor = self.services.get_descriptor(handle) - if not descriptor: - raise BleakError(f"Descriptor with handle {handle} was not found!") - gatt_desc = cast(GattDescriptor, descriptor.obj) value = bytearray( @@ -847,11 +844,11 @@ async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: 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 @@ -884,11 +881,13 @@ async def write_gatt_char( ) @override - 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). """ @@ -897,10 +896,6 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: assert self.services - descriptor = self.services.get_descriptor(handle) - if not descriptor: - raise BleakError(f"Descriptor with handle {handle} was not found!") - buf = WinBuffer(len(data)) buf.length = buf.capacity @@ -912,10 +907,10 @@ async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: _ensure_success( 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( From 977f339388f4a927f041a59c0012dc663975a26d Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 May 2025 21:52:03 -0500 Subject: [PATCH 086/113] backends: service: fix a type error with cast Descriptors aren't normally looked up by UUID because they are likely to have duplicates since each characteristic uses the same standard descriptors. So just cast to ignore the type error that the descriptor lookup function doesn't accept a UUID/str. --- bleak/backends/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bleak/backends/service.py b/bleak/backends/service.py index f8d85a199..5d8a6f247 100644 --- a/bleak/backends/service.py +++ b/bleak/backends/service.py @@ -7,7 +7,7 @@ """ import logging from collections.abc import Iterator -from typing import Any, Optional, Union +from typing import Any, Optional, Union, cast from uuid import UUID from bleak.backends.characteristic import BleakGATTCharacteristic @@ -102,7 +102,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]: From 689f21ef322b94b6c52999e1fc34917ddc2d9a37 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 23 May 2025 11:07:04 -0500 Subject: [PATCH 087/113] backends: winrt: don't wait for disconnect We have found that if another app is also connected to the same BLE device, we never get the GATT session close event. This causes the `disconnect()` method to hang forever. Instead, just close the GATT session and return immediately (and hope for the best). Fixes: https://github.com/hbldh/bleak/issues/1759 --- CHANGELOG.rst | 1 + bleak/backends/winrt/client.py | 25 ++++--------------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b77507849..f2797252e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -514,6 +514,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 ------- diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 1b48cb314..9a0c6dbe2 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -172,7 +172,6 @@ def __init__( 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: Optional[GattSession] = None self._notification_callbacks: dict[int, EventRegistrationToken] = {} @@ -308,9 +307,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 @@ -479,26 +475,13 @@ async def disconnect(self) -> None: 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) + self._requester.close() + self._requester = None @property @override From 70eb53aaa6cd6047eb521dba9c972f39bab1669f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 23 May 2025 11:24:48 -0500 Subject: [PATCH 088/113] move "Created on" out of doc comments It is a bit odd to have these show up in the html docs. And we already have the credits page there for that. --- bleak/backends/__init__.py | 4 +--- bleak/backends/characteristic.py | 4 +--- bleak/backends/client.py | 4 +--- bleak/backends/corebluetooth/CentralManagerDelegate.py | 4 +--- bleak/backends/corebluetooth/__init__.py | 4 +--- bleak/backends/corebluetooth/client.py | 3 +-- bleak/backends/descriptor.py | 4 +--- bleak/backends/device.py | 4 +--- bleak/backends/service.py | 4 +--- bleak/backends/winrt/client.py | 3 +-- 10 files changed, 10 insertions(+), 28 deletions(-) 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/characteristic.py b/bleak/backends/characteristic.py index c01eecf4f..f93a10abb 100644 --- a/bleak/backends/characteristic.py +++ b/bleak/backends/characteristic.py @@ -1,9 +1,7 @@ # -*- 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 - """ from __future__ import annotations diff --git a/bleak/backends/client.py b/bleak/backends/client.py index 7b0dd8d33..fdf07b458 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -1,9 +1,7 @@ # -*- 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 os diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index f9b0cc771..efd8c400f 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -1,9 +1,7 @@ +# 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 diff --git a/bleak/backends/corebluetooth/__init__.py b/bleak/backends/corebluetooth/__init__.py index 1bbe4f840..696ae6aa7 100644 --- a/bleak/backends/corebluetooth/__init__.py +++ b/bleak/backends/corebluetooth/__init__.py @@ -1,9 +1,7 @@ # -*- 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 diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 0589c7a16..2f34d1a1f 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -1,7 +1,6 @@ +# Created on 2019-06-26 by kevincar """ BLE Client for CoreBluetooth on macOS - -Created on 2019-06-26 by kevincar """ import sys diff --git a/bleak/backends/descriptor.py b/bleak/backends/descriptor.py index bed6dcf8d..6cf7fd561 100644 --- a/bleak/backends/descriptor.py +++ b/bleak/backends/descriptor.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- +# Created on 2019-03-19 by hbldh """ Interface class for the Bleak representation of a GATT Descriptor - -Created on 2019-03-19 by hbldh - """ from __future__ import annotations diff --git a/bleak/backends/device.py b/bleak/backends/device.py index 540f823a3..22387f4c7 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 - """ diff --git a/bleak/backends/service.py b/bleak/backends/service.py index 5d8a6f247..3bbf56469 100644 --- a/bleak/backends/service.py +++ b/bleak/backends/service.py @@ -1,9 +1,7 @@ # -*- 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 logging from collections.abc import Iterator diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 9a0c6dbe2..c57df92cd 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -1,8 +1,7 @@ # -*- 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 From a52dcca76cf962f43adce6b414d8aefbb24bfe91 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 23 May 2025 11:29:31 -0500 Subject: [PATCH 089/113] AUTHORS: acknowledge contributors who did not add their name to this file --- AUTHORS.rst | 2 ++ 1 file changed, 2 insertions(+) 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 -------- From 7b4e7bc0f057ef874a4e70d397b8db039444502e Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 23 May 2025 11:38:11 -0500 Subject: [PATCH 090/113] typings: fix a couple mypy complaints --- typings/Foundation/__init__.pyi | 14 +++++++++++--- typings/objc/__init__.pyi | 3 +-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/typings/Foundation/__init__.pyi b/typings/Foundation/__init__.pyi index b4aa70cf5..669dda24a 100644 --- a/typings/Foundation/__init__.pyi +++ b/typings/Foundation/__init__.pyi @@ -1,6 +1,6 @@ import sys -from collections.abc import Mapping, Sequence -from typing import Any, NewType, Optional, TypeVar +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 @@ -27,7 +27,10 @@ class NSObject: ) -> None: ... def removeObserver_forKeyPath_(self, observer: NSObject, keyPath: str) -> None: ... -class NSDictionary(NSObject, Mapping[str, Any]): ... +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 @@ -45,6 +48,11 @@ class NSData(NSObject, Buffer): 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): ... diff --git a/typings/objc/__init__.pyi b/typings/objc/__init__.pyi index b0a295853..54e8e9a4b 100644 --- a/typings/objc/__init__.pyi +++ b/typings/objc/__init__.pyi @@ -1,4 +1,4 @@ -from typing import Any, Literal, Optional, TypeVar, overload +from typing import Literal, Optional, TypeVar, overload from CoreBluetooth import CBCentralManagerDelegate, CBPeripheralDelegate from Foundation import NSObject @@ -21,4 +21,3 @@ def protocolNamed( def protocolNamed( name: Literal["CBPeripheralDelegate"], ) -> type[CBPeripheralDelegate]: ... -def protocolNamed(name: str) -> type[Any]: ... From a778e1a2edf62e54b9e4894c9a8e27d79c2c3693 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 23 May 2025 11:43:07 -0500 Subject: [PATCH 091/113] backends/characteristic: fix type hint bleak/backends/bluezdbus/manager.py:713: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance bleak/backends/bluezdbus/manager.py:713: note: Consider using "Sequence" instead, which is covariant --- bleak/backends/characteristic.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bleak/backends/characteristic.py b/bleak/backends/characteristic.py index f93a10abb..15bf8e3ed 100644 --- a/bleak/backends/characteristic.py +++ b/bleak/backends/characteristic.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Union from uuid import UUID +from bleak.assigned_numbers import CharacteristicPropertyName from bleak.backends.descriptor import BleakGATTDescriptor from bleak.uuids import normalize_uuid_str, uuidstr_to_str @@ -39,7 +40,7 @@ def __init__( obj: Any, handle: int, uuid: str, - properties: list[str], + properties: list[CharacteristicPropertyName], max_write_without_response_size: Callable[[], int], service: BleakGATTService, ): @@ -90,7 +91,7 @@ def description(self) -> str: return uuidstr_to_str(self.uuid) @property - def properties(self) -> list[str]: + def properties(self) -> list[CharacteristicPropertyName]: """Properties of this characteristic""" return self._properties From 6e78cca1dd43eb7d6a3ac6b935739169e3748eee Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 24 May 2025 11:50:11 -0500 Subject: [PATCH 092/113] backends: winrt: move protection_level out of parameters We want to deprecate this, so it shouldn't be part of the public API. It still works thanks to kwargs. --- bleak/backends/winrt/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index c57df92cd..dd39bd40a 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -506,7 +506,6 @@ def mtu_size(self) -> int: @override async def pair( self, - protection_level: Optional[DevicePairingProtectionLevel] = None, **kwargs: Any, ) -> None: """Attempts to pair with the device. @@ -533,6 +532,8 @@ async def pair( logging.debug("Device is already paired. Skipping pairing.") return + protection_level = kwargs.get("protection_level") + # Currently only supporting Just Works solutions... ceremony = DevicePairingKinds.CONFIRM_ONLY custom_pairing = device_information.pairing.custom From 90f1890f23921f5408566454e729ede210fced42 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 24 May 2025 11:52:01 -0500 Subject: [PATCH 093/113] backends: winrt: swap order of is_paired and can_pair check Apparently, can_pair is False if a device is already paired, so we need to reverse the order of the checks in order to avoid raising an exception when trying to pair a device that is already paired. --- bleak/backends/winrt/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index dd39bd40a..bccb5227b 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -525,13 +525,13 @@ async def pair( self._requester.device_information.id ) - if not device_information.pairing.can_pair: - raise BleakError("Device does not support pairing") - 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... From f1f855d8dbf275e7885f9eae9321e68844d4c952 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 24 May 2025 12:34:27 -0500 Subject: [PATCH 094/113] backends: winrt: improve pairing error message Recent versions of Python only print the int value of IntEnum. Printing the name is more user-friendly. --- bleak/backends/winrt/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index bccb5227b..e5def1831 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -563,7 +563,9 @@ def handler( DevicePairingResultStatus.PAIRED, DevicePairingResultStatus.ALREADY_PAIRED, ): - raise BleakError(f"Could not pair with device: {pairing_result.status}") + raise BleakError( + f"Could not pair with device: {pairing_result.status.name}" + ) logger.debug( "Paired to device with protection level %r.", From 97d5cacbdcb78a4e15d570cde91b19dad049a238 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 24 May 2025 15:04:58 -0500 Subject: [PATCH 095/113] backends: winrt: fix pair debug message with wrong value Seems to be a bug in the WinRT API where the value returned by the pairing status result is not updated unless we request a device info update first. --- bleak/backends/winrt/client.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index e5def1831..e221fe6b2 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -567,10 +567,18 @@ def handler( f"Could not pair with device: {pairing_result.status.name}" ) - logger.debug( - "Paired to device with protection level %r.", - pairing_result.protection_level_used, - ) + 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 + ) + + logger.debug( + "Paired to device with protection level %s.", + pairing_result.protection_level_used.name, + ) @override async def unpair(self) -> None: From d674a0c82a9947fc58dbe025b7f5faf062012859 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 24 May 2025 15:06:49 -0500 Subject: [PATCH 096/113] backends: winrt: try highest pairing level first This should remove the need for the protection_level kwarg. Not sure if the retry is actually needed since on the device used for testing, it connects at the ENCRYPTION level on the first try. --- bleak/backends/winrt/client.py | 19 ++++++++++++++++++- examples/philips_hue.py | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index e221fe6b2..4336cdf5b 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -552,7 +552,24 @@ def handler( ceremony, protection_level ) else: - pairing_result = await custom_pairing.pair_async(ceremony) + 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 diff --git a/examples/philips_hue.py b/examples/philips_hue.py index 23b3cdf96..331efb0d1 100644 --- a/examples/philips_hue.py +++ b/examples/philips_hue.py @@ -49,7 +49,7 @@ async def main(address: str): async with BleakClient(address) as client: print(f"Connected: {client.is_connected}") - await client.pair(protection_level=2) + await client.pair() print("Paired:") print("Turning Light off...") From 60f483f0397ae8d10ad4b99c96cc41e4703bc111 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 24 May 2025 15:17:54 -0500 Subject: [PATCH 097/113] backends: winrt: deprecate protection_level Now that the default behavior is to try to use the highest protection level, there should no longer be a need to manually set the protection level. --- CHANGELOG.rst | 1 + bleak/backends/winrt/client.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f2797252e..808606056 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Changed * 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. +* Deprecated ``protection_level`` kwarg for pairing in WinRT backend. Fixed ----- diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 4336cdf5b..95fe5ac1a 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -5,6 +5,7 @@ """ import sys from typing import TYPE_CHECKING +from warnings import warn if TYPE_CHECKING: if sys.platform != "win32": @@ -516,7 +517,12 @@ async def pair( 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...) + encryption and authentication. + + .. versionchanged:: unreleased + Issues :class:`DeprecationWarning` if used. The default + behavior has changed and this argument should no longer + be needed. """ assert self._requester @@ -548,6 +554,11 @@ def handler( 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 ) From b8d89fb63e54e0bddba72df93bbc4af355e2d131 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Wed, 28 May 2025 20:17:55 -0500 Subject: [PATCH 098/113] github: add stale bot messages Currently, the bot only adds labels. We want it to also comment so that notifications are sent to issue subscribers. --- .github/workflows/stale_issues.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/stale_issues.yml b/.github/workflows/stale_issues.yml index c0ab06bfc..9cd840cbb 100644 --- a/.github/workflows/stale_issues.yml +++ b/.github/workflows/stale_issues.yml @@ -12,3 +12,5 @@ jobs: 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. From aacc0eb50559772175aa0feb9800ae91207d01a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 08:14:18 +0000 Subject: [PATCH 099/113] build(deps-dev): bump requests from 2.32.0 to 2.32.4 Bumps [requests](https://github.com/psf/requests) from 2.32.0 to 2.32.4. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.0...v2.32.4) --- updated-dependencies: - dependency-name: requests dependency-version: 2.32.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/poetry.lock b/poetry.lock index 011ee3da7..c55472df5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -95,8 +95,7 @@ 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\"" +groups = ["dev", "docs"] files = [ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, @@ -108,8 +107,7 @@ 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\"" +groups = ["dev", "docs"] 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"}, @@ -320,8 +318,7 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" -groups = ["docs"] -markers = "python_version >= \"3.11\"" +groups = ["dev", "docs"] files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, @@ -705,20 +702,19 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[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\"" +groups = ["dev", "docs"] 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" @@ -997,8 +993,7 @@ version = "1.26.19" 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" -groups = ["docs"] -markers = "python_version >= \"3.11\"" +groups = ["dev", "docs"] files = [ {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, From b0cc7634fd45c08b7f58325641a7d98fc6118de5 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 12 Jun 2025 02:35:42 +1000 Subject: [PATCH 100/113] backends: winrt: scanner: enable extended advertising Enable extended advertising scanning by default by setting the `AllowExtendedAdvertisements` property, which defaults to false. https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementwatcher.allowextendedadvertisements Signed-off-by: Jordan Yates --- CHANGELOG.rst | 1 + bleak/backends/winrt/scanner.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 808606056..3ba0c19b5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,7 @@ Added Changed ------- +* Enabled extended advertising scanning in the WinRT backend. * Made `response` argument of class ``BleakClient.write_gatt_char()`` explicitly optional. Fixes #1730. * Updated Poetry build system version to ``>=2.0``. Merged #1718. * Log to stderr instead of stdout when ``BLEAK_LOGGING`` is enabled. Merged #1709. diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index 053995e4d..8b4bdbe8d 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -245,6 +245,8 @@ 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() From 6438da53ada4964d05f16baac373790fbf4afb57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 22:41:04 +0000 Subject: [PATCH 101/113] build(deps-dev): bump urllib3 from 1.26.19 to 2.5.0 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.19 to 2.5.0. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.19...2.5.0) --- updated-dependencies: - dependency-name: urllib3 dependency-version: 2.5.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/poetry.lock b/poetry.lock index c55472df5..b8f911868 100644 --- a/poetry.lock +++ b/poetry.lock @@ -95,7 +95,8 @@ version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["dev", "docs"] +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"}, @@ -107,7 +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 = ["dev", "docs"] +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"}, @@ -318,7 +320,8 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" -groups = ["dev", "docs"] +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"}, @@ -706,7 +709,8 @@ version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -groups = ["dev", "docs"] +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, @@ -989,20 +993,21 @@ markers = {main = "python_version < \"3.12\" or platform_system == \"Windows\"", [[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 = ["dev", "docs"] 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) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "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" From fe82d0624ddf7856a979f257fb20e06cf4e696d7 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 21 Jun 2025 16:52:10 -0500 Subject: [PATCH 102/113] bleak: use longer timeout when pairing during connect Add some docs and update the sample code to advise users to use a longer timeout when pairing during connect for devices that require user interaction. --- bleak/__init__.py | 4 ++++ examples/service_explorer.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/bleak/__init__.py b/bleak/__init__.py index dbf1fbaee..6c09f1d63 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -450,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 diff --git a/examples/service_explorer.py b/examples/service_explorer.py index 752d94ded..ffd5b85e1 100644 --- a/examples/service_explorer.py +++ b/examples/service_explorer.py @@ -54,6 +54,8 @@ async def main(args: Args): 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") From fa784a18f368aaea107865857853b97ef7a6cf3f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 21 Jun 2025 17:53:05 -0500 Subject: [PATCH 103/113] backends: bluez: handle address change after pairing Devices with resolvable private addresses will change their address after pairing. So for the address property to return the "correct" value, we need to update the device's address after pairing. Fixes: https://github.com/hbldh/bleak/issues/1737 --- CHANGELOG.rst | 1 + bleak/backends/bluezdbus/client.py | 19 +++++++++++++++++++ bleak/backends/bluezdbus/manager.py | 15 +++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3ba0c19b5..9cceb39a3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -29,6 +29,7 @@ Changed 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. diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 68c0e724b..64dcc79b0 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -284,6 +284,15 @@ async def disconnect_device() -> None: 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( @@ -509,6 +518,16 @@ async def pair(self, *args: Any, **kwargs: Any) -> None: assert reply assert_reply(reply) + # 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.""" diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index 485cef849..0bb324db6 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -753,6 +753,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. From cefe9d8c9f110f99ed7d9d3c83fa5d2003d0144f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 21 Jun 2025 18:04:44 -0500 Subject: [PATCH 104/113] bleak: use backend-specific key for seen_devices Replace use of address with backend-specific key for the seen_devices dictionary. As we found out, in the BlueZ backend, the address may not match the D-Bus path in the case of private resolvable addresses. So to avoid problems with converting the address to a D-Bus path, we need to use the D-Bus path as the key in the seen_devices dictionary. For good measure, we also use the macOS UUID for the device even when using the use_bdaddr hack to get the actual address. Other backends continue to use the address as the key, as there doesn't appear to be any better alternative. --- bleak/__init__.py | 2 +- bleak/backends/bluezdbus/scanner.py | 5 ++--- bleak/backends/bluezdbus/utils.py | 13 ------------- bleak/backends/corebluetooth/scanner.py | 1 + bleak/backends/p4android/scanner.py | 1 + bleak/backends/scanner.py | 14 +++++++++++--- bleak/backends/winrt/scanner.py | 2 +- 7 files changed, 17 insertions(+), 21 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index 6c09f1d63..667ce05a2 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -292,7 +292,7 @@ def discovered_devices_and_advertisement_data( .. versionadded:: 0.19 """ - return self._backend.seen_devices + return {d[0].address: d for d in self._backend.seen_devices.values()} @classmethod async def find_device_by_address( diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index a0069153f..dc62a700f 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -19,7 +19,6 @@ from bleak.args.bluez import BlueZScannerArgs from bleak.backends.bluezdbus.defs import Device1 from bleak.backends.bluezdbus.manager import get_global_bluez_manager -from bleak.backends.bluezdbus.utils import bdaddr_from_device_path from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, @@ -199,6 +198,7 @@ def _handle_advertising_data(self, path: str, props: Device1) -> None: ) device = self.create_or_update_device( + path, props["Address"], # BlueZ generates a name based on the address if no name is available. # To match other backends, we replace this with None. @@ -218,8 +218,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/utils.py b/bleak/backends/bluezdbus/utils.py index 74f09ec1d..46a9879f2 100644 --- a/bleak/backends/bluezdbus/utils.py +++ b/bleak/backends/bluezdbus/utils.py @@ -35,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. diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index a55bc4126..8e06a3852 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -148,6 +148,7 @@ def callback(p: CBPeripheral, a: NSDictionary, 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()), diff --git a/bleak/backends/p4android/scanner.py b/bleak/backends/p4android/scanner.py index 6db880b85..7476f2d4e 100644 --- a/bleak/backends/p4android/scanner.py +++ b/bleak/backends/p4android/scanner.py @@ -267,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/scanner.py b/bleak/backends/scanner.py index a329868ee..05ebfda24 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -115,6 +115,8 @@ class BaseBleakScanner(abc.ABC): """ 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. """ @@ -236,12 +238,18 @@ def call_detection_callbacks( callback(device, advertisement_data) def create_or_update_device( - self, address: str, name: Optional[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. @@ -252,13 +260,13 @@ def create_or_update_device( """ try: - device, _ = self.seen_devices[address] + device, _ = self.seen_devices[key] device.name = name except KeyError: device = BLEDevice(address, name, details) - self.seen_devices[address] = (device, adv) + self.seen_devices[key] = (device, adv) return device diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index 8b4bdbe8d..d25318f2e 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -212,7 +212,7 @@ def _received_handler( ) 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) From 69f634ed796f42715afc040862ec274620eb7394 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 28 Jun 2025 17:05:07 -0500 Subject: [PATCH 105/113] v1.0.0 Way overdue to use a real major version number. :sweat_smile: --- CHANGELOG.rst | 16 ++++++++++------ bleak/__init__.py | 12 ++++++------ bleak/backends/winrt/client.py | 2 +- pyproject.toml | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9cceb39a3..cdcc5f1e2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,9 @@ 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. -* Deprecated ``protection_level`` kwarg for pairing in WinRT backend. +* ``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 ----- @@ -33,13 +36,13 @@ Fixed * 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, ...) +* 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 deprecated parameters, properties and methods. * Removed support for macOS < 10.13. * Removed support for BlueZ < 5.55. @@ -1081,7 +1084,8 @@ Fixed * Bleak created. -.. _Unreleased: https://github.com/hbldh/bleak/compare/v0.22.3...develop +.. _Unreleased: https://github.com/hbldh/bleak/compare/v1.0.0...develop +.. _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/bleak/__init__.py b/bleak/__init__.py index 667ce05a2..f02e981e4 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -474,7 +474,7 @@ class BleakClient: No longer is alias for backend type and no longer inherits from :class:`BaseBleakClient`. Added ``backend`` parameter. - .. versionchanged:: unreleased + .. versionchanged:: 1.0 Added ``pair`` parameter. """ @@ -560,7 +560,7 @@ async def connect(self, **kwargs: Any) -> None: Args: **kwargs: For backwards compatibility - should not be used. - .. versionchanged:: unreleased + .. versionchanged:: 1.0 No longer returns ``True``. Instead, the return type is ``None``. """ await self._backend.connect(self._pair_before_connect, **kwargs) @@ -568,7 +568,7 @@ async def connect(self, **kwargs: Any) -> None: async def disconnect(self) -> None: """Disconnect from the specified GATT server. - .. versionchanged:: unreleased + .. versionchanged:: 1.0 No longer returns ``True``. Instead, the return type is ``None``. """ await self._backend.disconnect() @@ -582,7 +582,7 @@ async def pair(self, *args: Any, **kwargs: Any) -> None: that a characteristic that requires authentication is read or written. This method may have backend-specific additional keyword arguments. - .. versionchanged:: unreleased + .. versionchanged:: 1.0 No longer returns ``True``. Instead, the return type is ``None``. """ await self._backend.pair(*args, **kwargs) @@ -596,7 +596,7 @@ async def unpair(self) -> None: This method is only available on Windows and Linux and will raise an exception on other platforms. - .. versionchanged:: unreleased + .. versionchanged:: 1.0 No longer returns ``True``. Instead, the return type is ``None``. """ await self._backend.unpair() @@ -764,7 +764,7 @@ def callback(sender: BleakGATTCharacteristic, data: bytearray): .. versionchanged:: 0.18 The first argument of the callback is now a :class:`BleakGATTCharacteristic` instead of an ``int``. - .. versionchanged:: unreleased + .. versionchanged:: 1.0 Added the ``cb`` parameter. """ if not self.is_connected: diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 95fe5ac1a..1d6339adb 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -519,7 +519,7 @@ async def pair( 3. EncryptionAndAuthentication - Pair the device using encryption and authentication. - .. versionchanged:: unreleased + .. versionchanged:: 1.0 Issues :class:`DeprecationWarning` if used. The default behavior has changed and this argument should no longer be needed. diff --git a/pyproject.toml b/pyproject.toml index bf6e56c69..306d9d02e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "bleak" -version = "0.22.3" +version = "1.0.0" description = "Bluetooth Low Energy platform Agnostic Klient" authors = [{ name = "Henrik Blidh", email = "henrik.blidh@nedomkull.com" }] license = "MIT" From 7304b4fa27fae282f8f32d43fbc33ad91052e34d Mon Sep 17 00:00:00 2001 From: Alexandre Detiste Date: Sun, 29 Jun 2025 18:50:59 +0200 Subject: [PATCH 106/113] "tomli" was included in Python 3.11 as "tomllib" (#1782) Use tomllib instead of tomli when building the docs since they require Python 3.11 to build already. This required some trickery to make isort happy since the docs have a different minimum Python version than the rest of the code. --- .github/workflows/format_and_lint.yml | 2 +- docs/.isort.cfg | 2 ++ docs/conf.py | 5 ++--- poetry.lock | 18 ++++++++++-------- pyproject.toml | 5 ++--- 5 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 docs/.isort.cfg 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/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/conf.py b/docs/conf.py index 0e7b2d06e..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)["project"]["version"] + release = tomllib.load(f)["project"]["version"] # The short X.Y version. version = ".".join(release.split(".")[:2]) diff --git a/poetry.lock b/poetry.lock index b8f911868..be6735fc6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 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" @@ -15,15 +15,15 @@ files = [ [[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]] @@ -942,7 +942,7 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["docs", "lint", "test"] +groups = ["lint", "test"] files = [ {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"}, @@ -977,6 +977,7 @@ files = [ {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" @@ -997,7 +998,8 @@ version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] +groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, @@ -1273,4 +1275,4 @@ all = ["winrt-Windows.Foundation.Collections[all] (==3.1.0)", "winrt-Windows.Fou [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "9d339c9c074fa90012b04438993916d8c712d9aee4f7e1d70a8a86cbe5580984" +content-hash = "26e51524733733c0430cebba61666dcf25bbd138ba4269a69fad6de67a41e374" diff --git a/pyproject.toml b/pyproject.toml index 306d9d02e..694fad8f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ optional = true [tool.poetry.group.docs.dependencies] Sphinx = { version = ">=8.2.3", python = ">=3.11" } sphinx-rtd-theme = { version = ">=3.0.2", python = ">=3.11" } -tomli = ">=2.2.1" [tool.poetry.group.lint.dependencies] black = ">=24.3,<25.0" @@ -65,9 +64,9 @@ build-backend = "poetry.core.masonry.api" [tool.isort] profile = "black" -py_version=38 +py_version=39 src_paths = ["bleak", "examples", "tests"] -extend_skip = [".buildozer"] +extend_skip = [".buildozer", "docs"] [tool.mypy] python_version = "3.9" From acf7f5923d8d2ffa03eda9daf40ad0cf01c1f9a6 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 29 Jun 2025 12:28:37 -0500 Subject: [PATCH 107/113] bluez: client: fix a few static typing complaints - unnecessary cast - accessing attributes of None --- bleak/backends/bluezdbus/client.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 64dcc79b0..b3c25b1d9 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -15,7 +15,7 @@ import warnings from collections.abc import Callable from contextlib import AsyncExitStack -from typing import Any, Optional, Union, cast +from typing import Any, Optional, Union if sys.version_info < (3, 12): from typing_extensions import Buffer, override @@ -594,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: @@ -899,8 +904,6 @@ async def start_notify( """ Activate notifications/indications on a characteristic. """ - characteristic = cast(BleakGATTCharacteristic, characteristic) - self._notification_callbacks[characteristic.obj[0]] = callback assert self._bus is not None @@ -927,6 +930,8 @@ async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None: if not self.is_connected: raise BleakError("Not connected") + assert self._bus is not None + reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, From a0acd55a671ea78c89c305a6ca1a9868022d0f37 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 30 Jun 2025 11:04:16 -0500 Subject: [PATCH 108/113] backends/device: restore **kwargs in BLEDevice constructor The BLEDevice() constructor technically isn't considered part of the public API, so a breaking change was made removing `rssi` and `**kwargs`. However, there are users that implement their own backends, so effectively this should be considered public and the breaking change reverted. It looks like all users are passing `rssi` as a keyword argument, so just restoring `**kwargs` should be sufficient. Signed-off-by: David Lechner --- CHANGELOG.rst | 5 +++++ bleak/backends/device.py | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cdcc5f1e2..efbb8d657 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,11 @@ and this project adheres to `Semantic Versioning Date: Mon, 30 Jun 2025 11:54:06 -0500 Subject: [PATCH 109/113] backends: bluezdbus: restore deprecated imports There were some imports that were moved to `bleak.args.bluez` in v1.0 without a proper deprecation period. Restore these imports and add the proper deprecation warnings. --- CHANGELOG.rst | 6 ++ .../bluezdbus/advertisement_monitor.py | 24 +++++++- bleak/backends/bluezdbus/manager.py | 6 +- bleak/backends/bluezdbus/scanner.py | 23 +++++++- bleak/backends/corebluetooth/client.py | 4 +- bleak/backends/corebluetooth/scanner.py | 16 +++++- bleak/backends/winrt/client.py | 17 +++++- .../bluezdbus/test_deprecated_imports.py | 55 +++++++++++++++++++ .../backends/winrt/test_deprecated_imports.py | 20 +++++++ tests/bleak/corebluetooth/__init__.py | 0 .../corebluetooth/test_deprecated_imports.py | 20 +++++++ 11 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 tests/bleak/backends/bluezdbus/test_deprecated_imports.py create mode 100644 tests/bleak/backends/winrt/test_deprecated_imports.py create mode 100644 tests/bleak/corebluetooth/__init__.py create mode 100644 tests/bleak/corebluetooth/test_deprecated_imports.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index efbb8d657..330406625 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,10 +10,16 @@ and this project adheres to `Semantic Versioning GattCommunicationStatus: ... @@ -152,7 +163,7 @@ def __init__( address_or_ble_device: Union[BLEDevice, str], services: Optional[set[str]] = None, *, - winrt: WinRTClientArgs, + winrt: _WinRTClientArgs, **kwargs: Any, ): super(BleakClientWinRT, self).__init__(address_or_ble_device, **kwargs) 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/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__ From a46d9b6de09b749a591f3594585359f10e96a7c0 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 30 Jun 2025 18:01:10 -0500 Subject: [PATCH 110/113] github: add -v option to pytest This will let us see which tests are actually being run, which is useful since some tests are skipped on some platforms. --- .github/workflows/build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index d495176cf..5ef2053d3 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -33,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: From bf224c0230ed9096429a21d782b5b4d9d7ab7bd7 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 30 Jun 2025 18:07:33 -0500 Subject: [PATCH 111/113] v1.0.1 --- CHANGELOG.rst | 6 +++++- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 330406625..bdffec57e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,9 @@ and this project adheres to `Semantic Versioning Date: Sun, 20 Jul 2025 18:01:42 +0200 Subject: [PATCH 112/113] Update service_explorer.py --- examples/service_explorer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/service_explorer.py b/examples/service_explorer.py index ffd5b85e1..2bf6e8e05 100644 --- a/examples/service_explorer.py +++ b/examples/service_explorer.py @@ -65,7 +65,7 @@ async def main(args: Args): 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.handle) extra = f", Value: {value}" except Exception as e: extra = f", Error: {e}" From d65d1921ef478722ece2e610e4b6499885c6d5eb Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 20 Jul 2025 17:44:33 -0500 Subject: [PATCH 113/113] examples/service_explorer: pass objects directly instead of handles When reading characteristics and descriptors, we can pass the objects directly instead of their handles. This is more efficient because it saves a dictionary lookup to get the object from the handle. No point in doing that lookup if we already have the object. --- examples/service_explorer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/service_explorer.py b/examples/service_explorer.py index 2bf6e8e05..a2ef39351 100644 --- a/examples/service_explorer.py +++ b/examples/service_explorer.py @@ -65,7 +65,7 @@ async def main(args: Args): for char in service.characteristics: if "read" in char.properties: try: - value = await client.read_gatt_char(char.handle) + value = await client.read_gatt_char(char) extra = f", Value: {value}" except Exception as e: extra = f", Error: {e}" @@ -84,7 +84,7 @@ async def main(args: Args): 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)