pax_global_header00006660000000000000000000000064147730367520014530gustar00rootroot0000000000000052 comment=4f8c1837df93e09ac6e23755fe4493d5c809ee63 bleak-retry-connector-3.10.0/000077500000000000000000000000001477303675200160225ustar00rootroot00000000000000bleak-retry-connector-3.10.0/.all-contributorsrc000066400000000000000000000004741477303675200216600ustar00rootroot00000000000000{ "projectName": "bleak-retry-connector", "projectOwner": "bluetooth-devices", "repoType": "github", "repoHost": "https://github.com", "files": [ "README.md" ], "imageSize": 80, "commit": true, "commitConvention": "angular", "contributors": [], "contributorsPerLine": 7, "skipCi": true } bleak-retry-connector-3.10.0/.editorconfig000066400000000000000000000004441477303675200205010ustar00rootroot00000000000000# http://editorconfig.org root = true [*] indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 end_of_line = lf [*.bat] indent_style = tab end_of_line = crlf [LICENSE] insert_final_newline = false [Makefile] indent_style = tab bleak-retry-connector-3.10.0/.flake8000066400000000000000000000000561477303675200171760ustar00rootroot00000000000000[flake8] exclude = docs max-line-length = 120 bleak-retry-connector-3.10.0/.github/000077500000000000000000000000001477303675200173625ustar00rootroot00000000000000bleak-retry-connector-3.10.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001477303675200215455ustar00rootroot00000000000000bleak-retry-connector-3.10.0/.github/ISSUE_TEMPLATE/1-bug_report.md000066400000000000000000000004221477303675200243730ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve labels: bug --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: **Additional context** Add any other context about the problem here. bleak-retry-connector-3.10.0/.github/ISSUE_TEMPLATE/2-feature-request.md000066400000000000000000000006721477303675200253540ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project labels: enhancement --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Additional context** Add any other context or screenshots about the feature request here. bleak-retry-connector-3.10.0/.github/dependabot.yml000066400000000000000000000013431477303675200222130ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" commit-message: prefix: "chore(ci): " groups: github-actions: patterns: - "*" - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" bleak-retry-connector-3.10.0/.github/labels.toml000066400000000000000000000035151477303675200215250ustar00rootroot00000000000000[breaking] color = "ffcc00" name = "breaking" description = "Breaking change." [bug] color = "d73a4a" name = "bug" description = "Something isn't working" [dependencies] color = "0366d6" name = "dependencies" description = "Pull requests that update a dependency file" [github_actions] color = "000000" name = "github_actions" description = "Update of github actions" [documentation] color = "1bc4a5" name = "documentation" description = "Improvements or additions to documentation" [duplicate] color = "cfd3d7" name = "duplicate" description = "This issue or pull request already exists" [enhancement] color = "a2eeef" name = "enhancement" description = "New feature or request" ["good first issue"] color = "7057ff" name = "good first issue" description = "Good for newcomers" ["help wanted"] color = "008672" name = "help wanted" description = "Extra attention is needed" [invalid] color = "e4e669" name = "invalid" description = "This doesn't seem right" [nochangelog] color = "555555" name = "nochangelog" description = "Exclude pull requests from changelog" [question] color = "d876e3" name = "question" description = "Further information is requested" [removed] color = "e99695" name = "removed" description = "Removed piece of functionalities." [tests] color = "bfd4f2" name = "tests" description = "CI, CD and testing related changes" [wontfix] color = "ffffff" name = "wontfix" description = "This will not be worked on" [discussion] color = "c2e0c6" name = "discussion" description = "Some discussion around the project" [hacktoberfest] color = "ffa663" name = "hacktoberfest" description = "Good issues for Hacktoberfest" [answered] color = "0ee2b6" name = "answered" description = "Automatically closes as answered after a delay" [waiting] color = "5f7972" name = "waiting" description = "Automatically closes if no answer after a delay" bleak-retry-connector-3.10.0/.github/workflows/000077500000000000000000000000001477303675200214175ustar00rootroot00000000000000bleak-retry-connector-3.10.0/.github/workflows/ci.yml000066400000000000000000000047271477303675200225470ustar00rootroot00000000000000name: CI on: push: branches: - main pull_request: concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" - uses: pre-commit/action@v3.0.1 # Make sure commit messages follow the conventional commits convention: # https://www.conventionalcommits.org commitlint: name: Lint Commit Messages runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v6 test: strategy: fail-fast: false matrix: python-version: - "3.10" - "3.11" - "3.12" - "3.13" os: - ubuntu-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - uses: snok/install-poetry@v1 - name: Install Dependencies shell: bash run: poetry install - name: Test with Pytest shell: bash run: poetry run pytest --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} release: runs-on: ubuntu-latest environment: release if: github.ref == 'refs/heads/main' needs: - test - lint - commitlint permissions: id-token: write contents: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Run semantic release: # - Update CHANGELOG.md # - Update version in code # - Create git tag # - Create GitHub release - name: Python Semantic Release id: release uses: python-semantic-release/python-semantic-release@v9.21.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 if: steps.release.outputs.released == 'true' - name: Upload Github Release Assets uses: python-semantic-release/publish-action@v9.21.0 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ steps.release.outputs.tag }} bleak-retry-connector-3.10.0/.github/workflows/hacktoberfest.yml000066400000000000000000000005341477303675200247700ustar00rootroot00000000000000name: Hacktoberfest on: schedule: # Run every day in October - cron: "0 0 * 10 *" # Run on the 1st of November to revert - cron: "0 13 1 11 *" jobs: hacktoberfest: runs-on: ubuntu-latest steps: - uses: browniebroke/hacktoberfest-labeler-action@v2.3.0 with: github_token: ${{ secrets.GH_PAT }} bleak-retry-connector-3.10.0/.github/workflows/issue-manager.yml000066400000000000000000000013401477303675200247000ustar00rootroot00000000000000name: Issue Manager on: schedule: - cron: "0 0 * * *" issue_comment: types: - created issues: types: - labeled pull_request_target: types: - labeled workflow_dispatch: jobs: issue-manager: runs-on: ubuntu-latest steps: - uses: tiangolo/issue-manager@0.5.1 with: token: ${{ secrets.GITHUB_TOKEN }} config: > { "answered": { "message": "Assuming the original issue was solved, it will be automatically closed now." }, "waiting": { "message": "Automatically closing. To re-open, please provide the additional information requested." } } bleak-retry-connector-3.10.0/.github/workflows/labels.yml000066400000000000000000000007751477303675200234150ustar00rootroot00000000000000name: Sync Github labels on: push: branches: - main paths: - ".github/**" jobs: labels: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.11 - name: Install labels run: pip install labels - name: Sync config with Github run: labels -u ${{ github.repository_owner }} -t ${{ secrets.GITHUB_TOKEN }} sync -f .github/labels.toml bleak-retry-connector-3.10.0/.gitignore000066400000000000000000000040661477303675200200200ustar00rootroot00000000000000# Created by .ignore support plugin (hsz.mobi) ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ bleak-retry-connector-3.10.0/.gitpod.yml000066400000000000000000000003061477303675200201100ustar00rootroot00000000000000tasks: - command: | pip install poetry PIP_USER=false poetry install - command: | pip install pre-commit pre-commit install PIP_USER=false pre-commit install-hooks bleak-retry-connector-3.10.0/.idea/000077500000000000000000000000001477303675200170025ustar00rootroot00000000000000bleak-retry-connector-3.10.0/.idea/bleak-retry-connector.iml000066400000000000000000000005151477303675200237170ustar00rootroot00000000000000 bleak-retry-connector-3.10.0/.idea/watcherTasks.xml000066400000000000000000000052531477303675200221740ustar00rootroot00000000000000 bleak-retry-connector-3.10.0/.idea/workspace.xml000066400000000000000000000027411477303675200215260ustar00rootroot00000000000000 bleak-retry-connector-3.10.0/.pre-commit-config.yaml000066400000000000000000000034161477303675200223070ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks exclude: "CHANGELOG.md" default_stages: [pre-commit] ci: autofix_commit_msg: "chore(pre-commit.ci): auto fixes" autoupdate_commit_msg: "chore(pre-commit.ci): pre-commit autoupdate" repos: - repo: https://github.com/commitizen-tools/commitizen rev: v4.4.1 hooks: - id: commitizen stages: [commit-msg] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: debug-statements - id: check-builtin-literals - id: check-case-conflict - id: check-docstring-first - id: check-json - id: check-toml - id: check-xml - id: check-yaml - id: detect-private-key - id: end-of-file-fixer - id: trailing-whitespace - id: debug-statements - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 hooks: - id: prettier args: ["--tab-width", "2"] additional_dependencies: - prettier@2.8.4 - repo: https://github.com/asottile/pyupgrade rev: v3.19.1 hooks: - id: pyupgrade args: [--py310-plus] - repo: https://github.com/PyCQA/isort rev: 6.0.1 hooks: - id: isort - repo: https://github.com/psf/black rev: 25.1.0 hooks: - id: black - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell - repo: https://github.com/PyCQA/flake8 rev: 7.2.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.15.0 hooks: - id: mypy additional_dependencies: [] - repo: https://github.com/PyCQA/bandit rev: 1.8.3 hooks: - id: bandit args: [-x, tests] bleak-retry-connector-3.10.0/.readthedocs.yml000066400000000000000000000010051477303675200211040ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . extra_requirements: - docs bleak-retry-connector-3.10.0/CHANGELOG.md000066400000000000000000001376361477303675200176530ustar00rootroot00000000000000# CHANGELOG ## v3.10.0 (2025-04-01) ### Chores - Update dependabot.yml to increase GHA update frequency ([`53b72a1`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/53b72a18b7f889519eca49a9f5fb43199d56cb9b)) - Update deps ([#152](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/152), [`c4b0050`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/c4b0050678e01f2e92a92a419d8fd08f2f972045)) dependabot does not yet support new pyproject.toml format - **ci**: Bump the github-actions group across 1 directory with 8 updates ([#150](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/150), [`1f67cb0`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/1f67cb0df49102ddd64c9d19ee44f33d1ecffc5d)) * chore(ci): bump the github-actions group across 1 directory with 8 updates Bumps the github-actions group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `3` | `4` | | [actions/setup-python](https://github.com/actions/setup-python) | `3` | `5` | | [pre-commit/action](https://github.com/pre-commit/action) | `2.0.3` | `3.0.1` | | [wagoid/commitlint-github-action](https://github.com/wagoid/commitlint-github-action) | `4.1.11` | `6.2.1` | | [codecov/codecov-action](https://github.com/codecov/codecov-action) | `3` | `5` | | [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) | `7.34.6` | `9.21.0` | | [browniebroke/hacktoberfest-labeler-action](https://github.com/browniebroke/hacktoberfest-labeler-action) | `2.2.0` | `2.3.0` | | [tiangolo/issue-manager](https://github.com/tiangolo/issue-manager) | `0.4.0` | `0.5.1` | Updates `actions/checkout` from 3 to 4 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) Updates `actions/setup-python` from 3 to 5 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v5) Updates `pre-commit/action` from 2.0.3 to 3.0.1 - [Release notes](https://github.com/pre-commit/action/releases) - [Commits](https://github.com/pre-commit/action/compare/v2.0.3...v3.0.1) Updates `wagoid/commitlint-github-action` from 4.1.11 to 6.2.1 - [Changelog](https://github.com/wagoid/commitlint-github-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/wagoid/commitlint-github-action/compare/v4.1.11...v6.2.1) Updates `codecov/codecov-action` from 3 to 5 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v5) Updates `python-semantic-release/python-semantic-release` from 7.34.6 to 9.21.0 - [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) - [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.rst) - [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v7.34.6...v9.21.0) Updates `browniebroke/hacktoberfest-labeler-action` from 2.2.0 to 2.3.0 - [Release notes](https://github.com/browniebroke/hacktoberfest-labeler-action/releases) - [Changelog](https://github.com/browniebroke/hacktoberfest-labeler-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/browniebroke/hacktoberfest-labeler-action/compare/v2.2.0...v2.3.0) Updates `tiangolo/issue-manager` from 0.4.0 to 0.5.1 - [Release notes](https://github.com/tiangolo/issue-manager/releases) - [Commits](https://github.com/tiangolo/issue-manager/compare/0.4.0...0.5.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-python dependency-type: direct:production - dependency-name: pre-commit/action dependency-type: direct:production - dependency-name: wagoid/commitlint-github-action dependency-type: direct:production - dependency-name: codecov/codecov-action dependency-type: direct:production - dependency-name: python-semantic-release/python-semantic-release dependency-type: direct:production - dependency-name: browniebroke/hacktoberfest-labeler-action dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: tiangolo/issue-manager dependency-type: direct:production dependency-group: github-actions ... Signed-off-by: dependabot[bot] * chore: update pyproject * chore: adjust actions --------- Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston - **pre-commit.ci**: Pre-commit autoupdate ([#149](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/149), [`b71c2d6`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/b71c2d6f9a2fe8758a77da50a4e061e14106cd83)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#151](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/151), [`ace8adf`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/ace8adf7c33317406bca6fbe210643beabadfe67)) updates: - [github.com/commitizen-tools/commitizen: v4.2.2 → v4.4.1](https://github.com/commitizen-tools/commitizen/compare/v4.2.2...v4.4.1) - [github.com/PyCQA/isort: 6.0.0 → 6.0.1](https://github.com/PyCQA/isort/compare/6.0.0...6.0.1) - [github.com/PyCQA/flake8: 7.1.2 → 7.2.0](https://github.com/PyCQA/flake8/compare/7.1.2...7.2.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> ### Features - Switch to trusted publishing ([#153](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/153), [`4b8510c`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/4b8510c6fac4b0d640afbb8152f72ab383cd1dfd)) ## v3.9.0 (2025-02-20) ### Chores - Update dependabot.yml to include GHA ([`3158e20`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/3158e2074c11b99d2a11c21ccc197a1d1e93faf2)) - **pre-commit.ci**: Pre-commit autoupdate ([#146](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/146), [`c9b4072`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/c9b4072d099ec9ca842a7a406eb47569a5265092)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#147](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/147), [`18cd61e`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/18cd61ed26a6de86879d6f34a6603b47d8a16452)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> ### Features - Add internal stop_discovery method for habluetooth ([#148](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/148), [`ae9feac`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/ae9feac7138ace0ba62cb30112f29a3ce6f47b28)) ## v3.8.1 (2025-02-04) ### Bug Fixes - Update poetry to v2 ([#144](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/144), [`9b45308`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/9b45308e6055c4a42c50228b2e21b20e2adfd604)) ### Chores - **deps**: Bump bluetooth-adapters from 0.21.0 to 0.21.1 ([#139](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/139), [`d6a5e37`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/d6a5e37f639a644ca6beed7a707e521f30f59757)) Bumps [bluetooth-adapters](https://github.com/bluetooth-devices/bluetooth-adapters) from 0.21.0 to 0.21.1. - [Release notes](https://github.com/bluetooth-devices/bluetooth-adapters/releases) - [Changelog](https://github.com/Bluetooth-Devices/bluetooth-adapters/blob/main/CHANGELOG.md) - [Commits](https://github.com/bluetooth-devices/bluetooth-adapters/compare/v0.21.0...v0.21.1) --- updated-dependencies: - dependency-name: bluetooth-adapters dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump dbus-fast from 2.30.2 to 2.32.0 ([#141](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/141), [`060341f`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/060341f63492556c63dda1a90c39a4bcce5b0268)) - **deps-dev**: Bump pytest-asyncio from 0.25.2 to 0.25.3 ([#142](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/142), [`a29042c`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/a29042c36d3ce638990e63b2acd160dd7c4d1823)) - **pre-commit.ci**: Pre-commit autoupdate ([#140](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/140), [`6a4e839`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/6a4e839c039b9f527612499707030cc03c4399b1)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#143](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/143), [`87c49e5`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/87c49e5483a61f8d1a13bffc26c954d4aef7cdb3)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> ## v3.8.0 (2025-01-21) ### Chores - **deps**: Bump bluetooth-adapters from 0.20.2 to 0.21.0 ([#137](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/137), [`bc3045c`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/bc3045c1e44aea3a3075fabbe7172a7ac2a388dc)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump pytest-asyncio from 0.23.8 to 0.25.2 ([#136](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/136), [`d71425a`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/d71425a0298418ec6674772e74dc683f2cfcfbb3)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ### Features - Add method to fetch current allocations ([#138](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/138), [`4dc325b`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/4dc325b4db5afc613064ba1257987a4a0e00fa7e)) ## v3.7.0 (2025-01-18) ### Chores - Create dependabot.yml ([`bece8dd`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/bece8dd77ef736a24a8b4ede66472ded83bc5759)) - Update Python 3.13 in CI ([#127](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/127), [`200dc40`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/200dc403c398ab3f5b328e9a0cd644bfe6e27ba3)) - **deps**: Bump aiohttp from 3.9.1 to 3.10.11 ([#135](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/135), [`f713f8a`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/f713f8a8bd458e7cdea31f9982f20e008cccd073)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump bluetooth-adapters from 0.16.2 to 0.20.2 ([#128](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/128), [`c5f661d`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/c5f661d1d1e976bd66e42edc06b0633e42f1835d)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump dbus-fast from 2.21.0 to 2.30.2 ([#129](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/129), [`717149c`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/717149c289394b24b756a5e19cb024130ecee2a5)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump idna from 3.6 to 3.7 ([#134](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/134), [`8afeaca`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/8afeacabe79580958fdbade047bc1b9a00144324)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump pytest from 7.4.4 to 8.3.4 ([#131](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/131), [`9b5b266`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/9b5b266027e5b7eb523d438d72e5e9e7dbe89a3f)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump pytest-asyncio from 0.19.0 to 0.23.8 ([#130](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/130), [`1506d86`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/1506d866c80fe0dd6f246fd4e8599ed79cfb7c0d)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump pytest-cov from 3.0.0 to 6.0.0 ([#132](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/132), [`50a2912`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/50a291211532a186d8a1df8c7406e5185e9b4999)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#120](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/120), [`4732a33`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/4732a3346329c0532da07c745cc575cdd796cd52)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston - **pre-commit.ci**: Pre-commit autoupdate ([#121](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/121), [`4eebb4e`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/4eebb4e028a7935967ff3f2a2ac4e818a1f497d5)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#123](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/123), [`9d7a463`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/9d7a46368accb79622ec30ddee79c2f2beb3454c)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#124](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/124), [`7c8bd62`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/7c8bd629cee757661c58d00006b7d197aeb17410)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#125](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/125), [`edea1ad`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/edea1adc00ba0ea84e9eed1149057a6f37a30165)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#126](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/126), [`f009e05`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/f009e0501735d2db725b15404ac67be2e7171b4d)) ### Features - Add support for getting callbacks on slot allocation change ([#133](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/133), [`ae21ecb`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/ae21ecb8524555dceafbbe47fb5d3b62efd51f1a)) ## v3.6.0 (2024-10-05) ### Chores - **pre-commit.ci**: Pre-commit autoupdate ([#112](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/112), [`87b345f`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/87b345f05c27764232ff2aee0637fe2c9b1adf70)) * chore(pre-commit.ci): pre-commit autoupdate updates: - [github.com/commitizen-tools/commitizen: v2.42.0 → v3.27.0](https://github.com/commitizen-tools/commitizen/compare/v2.42.0...v3.27.0) - [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.6.0) - [github.com/pre-commit/mirrors-prettier: v2.7.1 → v4.0.0-alpha.8](https://github.com/pre-commit/mirrors-prettier/compare/v2.7.1...v4.0.0-alpha.8) - [github.com/asottile/pyupgrade: v3.3.1 → v3.16.0](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.16.0) - [github.com/PyCQA/isort: 5.12.0 → 5.13.2](https://github.com/PyCQA/isort/compare/5.12.0...5.13.2) - [github.com/psf/black: 23.1.0 → 24.4.2](https://github.com/psf/black/compare/23.1.0...24.4.2) - [github.com/codespell-project/codespell: v2.2.2 → v2.3.0](https://github.com/codespell-project/codespell/compare/v2.2.2...v2.3.0) - [github.com/PyCQA/flake8: 6.0.0 → 7.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...7.1.0) - [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.10.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.10.1) - [github.com/PyCQA/bandit: 1.7.4 → 1.7.9](https://github.com/PyCQA/bandit/compare/1.7.4...1.7.9) * chore(pre-commit.ci): auto fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#113](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/113), [`4226fa2`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/4226fa2569b02b1629cdc99e1be181dbdd568b37)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#114](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/114), [`92c271d`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/92c271dff0008fac509d602e0a7ea3b74c826f83)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#115](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/115), [`98c40cb`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/98c40cb482379f90e7861376ac9c391eaf05a08c)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#116](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/116), [`230b739`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/230b73900dfacddf5ba48f8ed2a2fc1d4f47edfe)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#117](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/117), [`ef36e57`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/ef36e57234aca7cd04fd0e7922b809f01b41b777)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#118](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/118), [`12b4f0b`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/12b4f0b09ef54044e519f52621a2b5b863b0efcc)) updates: - [github.com/commitizen-tools/commitizen: v3.29.0 → v3.29.1](https://github.com/commitizen-tools/commitizen/compare/v3.29.0...v3.29.1) - [github.com/PyCQA/bandit: 1.7.9 → 1.7.10](https://github.com/PyCQA/bandit/compare/1.7.9...1.7.10) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> ### Features - Add support for Python 3.13 ([#119](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/119), [`f2c3fa5`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/f2c3fa58217d4133b83aafd5ea885edc9e78ae85)) ## v3.5.0 (2024-04-10) ### Features - Add device path to the disconnect debug logging ([#111](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/111), [`8e010b3`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/8e010b3f3754b156e68699f5727be77d4f8412a3)) ## v3.4.0 (2024-01-01) ### Chores - Add python 3.12 to the CI ([#104](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/104), [`c6fac48`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/c6fac48a1dd240f0921d3e98736b8f08fad3428f)) ### Features - Add close_stale_connections_by_address ([#110](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/110), [`74de12f`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/74de12fb5a9269bce677cb76ae0f05daf1af343a)) ## v3.3.0 (2023-10-25) ### Features - Handle services changed during connecting ([#108](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/108), [`1c65413`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/1c65413cad1e6c42d6bf2c0a8cdec82d9d9a7484)) ## v3.2.1 (2023-09-14) ### Bug Fixes - Correct fetching the global bluez manager when its not running ([#106](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/106), [`38c63a9`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/38c63a974dd05f7a1e42d647e3ca13884e9b4e62)) ## v3.2.0 (2023-09-14) ### Chores - Log exception type when device disappears ([#105](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/105), [`445bd43`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/445bd43c62bb9f4afc60b3d359fa94aeaa3abd98)) ### Features - Remove devices on cache clear to cleanup disk cache ([#103](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/103), [`349e0de`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/349e0deeb2bee443e82d23b816bbd7036a476718)) ## v3.1.3 (2023-09-07) ### Bug Fixes - Ensure timeouts work with py3.11 ([#102](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/102), [`4951aef`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/4951aefd9de0e22235dbbd64a15357e67f496d87)) ## v3.1.2 (2023-09-03) ### Bug Fixes - Increase bleak safety timeout to allow for longer disconnect timeout ([#101](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/101), [`39380a7`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/39380a744b9aed832b51ad20671af86b99186560)) ## v3.1.1 (2023-07-25) ### Bug Fixes - Check more often for a device to reappear after the adapter runs out of slots ([#100](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/100), [`4c9c9c0`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/4c9c9c0670c79d9425e26b761a20d588dd259a26)) ## v3.1.0 (2023-07-19) ### Chores - Bump python-semantic-release to fix release process ([#98](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/98), [`ee8ebcb`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/ee8ebcbbe3761d32da6e8c542e2cfb10162706f2)) - Downgrade python-semantic-release ([#99](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/99), [`beee26f`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/beee26f53e5aae2ed78b7c05ea3e8c09b8c7c6a3)) - Fix ci ([#96](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/96), [`50da16b`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/50da16be853b075fabb7a6f81f85380a1ac15e86)) ### Features - Decrease backoff times ([#97](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/97), [`37b71c8`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/37b71c8bf1bd456de3d44ca4f7845de07c853bbc)) - Update the out of slots message to be more clear ([#95](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/95), [`9269a82`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/9269a82f5f1c88c382856c88e98102d1b83dc436)) ## v3.0.2 (2023-03-25) ### Bug Fixes - Bluez services cache clear was ineffective ([#93](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/93), [`ec86cb6`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/ec86cb6788ba075920867b5cb06d1f5fa49d18ae)) ## v3.0.1 (2023-03-18) ### Bug Fixes - Update for bleak 0.20.0 ([#92](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/92), [`78f9a1e`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/78f9a1e81768ee9543595a6c8673c8c635f63244)) ## v3.0.0 (2023-02-25) ### Bug Fixes - Bump python-semantic-release ([#90](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/90), [`c401988`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/c4019883c9bad3f91a20029e8adf35962a59a488)) - Lint ([#89](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/89), [`c3b5ff8`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/c3b5ff8870b8a5c6cb7972d9e1a0ca677cc0c78d)) - Typing for generic BleakClient classes and the retry_bluetooth_connection_error decorator ([#86](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/86), [`8ddf242`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/8ddf2426ff2fc5274dc2e8a905233a2c30f57fbb)) * fix: typing for the generic BleakClient client class Using a bound TypeVar we can ensure that any client class we are dealing with is either BleakClient or a descendant of it and that type then stays consistent throughout the lifecycle. Signed-off-by: Felix Kaechele * fix: typing for the retry_bluetooth_connection_error decorator Use TypeVar together with ParamVar to drop the use of the unsafe cast operation. --------- Co-authored-by: J. Nick Koston ### Chores - Drop Python 3.9 support ([#88](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/88), [`58f9958`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/58f9958785b40d2fbade39ef7f56dab931f888a6)) BREAKING CHANGE: In preparation for the use of Python 3.10 typing features such as ParamSpec, which is unavailable on Python 3.9. Following the schema of supporting the current and one previous Python release this drops support for Python 3.9. Signed-off-by: Felix Kaechele - Update pre-commit hooks ([#87](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/87), [`fd08a1c`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/fd08a1cf76ce012feadcf4a491b0c31f346b783b)) Co-authored-by: J. Nick Koston ### Breaking Changes - In preparation for the use of Python 3.10 typing features such as ParamSpec, which is unavailable on Python 3.9. ## v2.13.1 (2023-01-12) ### Bug Fixes - Make bluetooth-adapters install Linux only as well ([#85](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/85), [`910f0b7`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/910f0b7147c31d1133bc5d308d134a72e47c3ff5)) - Only import from bluetooth_adapters when running on linux ([#84](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/84), [`51926f7`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/51926f7a679437df875f7cb5b6e53253ae10f0b6)) ## v2.13.0 (2022-12-23) ### Features - Remove freshen fallback logic since Home Assistant always provides us the best path to the device now ([#83](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/83), [`0954d2d`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/0954d2dfc7ff06f3b7445140c644aeaf7ea36384)) ## v2.12.1 (2022-12-22) ### Bug Fixes - _on_characteristic_value_changed in BleakSlotManager should accept any arguments ([#82](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/82), [`71cc37e`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/71cc37ef6b0b7492fb58aaeb9115737e95bd9f0e)) ## v2.12.0 (2022-12-22) ### Features - Add utility function to get device_source ([#81](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/81), [`d72ce15`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/d72ce150edba658b4d4edb43f3bbd158cba9988f)) ## v2.11.0 (2022-12-22) ### Features - Add connection slot manager ([#80](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/80), [`d8bb8d9`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/d8bb8d96fb019152fb97e4006a8e6a1d11213a7d)) ## v2.10.2 (2022-12-12) ### Bug Fixes - Stop trying to get devices from bluez if dbus setup times out ([#78](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/78), [`a8da722`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/a8da7222d6d7ab725152141f560dc1bb681bf4cf)) ## v2.10.1 (2022-12-05) ### Bug Fixes - Optimize IS_LINUX check in restore_discoveries ([#77](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/77), [`f22eb33`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/f22eb33e1d29d5a6ca8697061de9fbb1bf583bec)) ## v2.10.0 (2022-12-05) ### Features - Add restore_discoveries to fix missing devices ([#76](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/76), [`f4432ac`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/f4432ac086abc0e847ac12818fa22cfaa04a3521)) ## v2.9.0 (2022-12-03) ### Features - Add function to clear the cache ([#75](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/75), [`6ca6011`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/6ca601104cd13cefa9c2d6db05cdc019aaf18329)) ## v2.8.9 (2022-12-02) ### Bug Fixes - Always log the connection attempt number ([#74](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/74), [`3306053`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/3306053a3903efa565355e2331b31db739bac094)) ## v2.8.8 (2022-12-02) ### Bug Fixes - Avoid logging connecting and connected since our BLEDevice may be stale ([#72](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/72), [`10e040c`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/10e040c9eb563d31b3e0caf41ee390234e239c4f)) ### Chores - Add py311 to the CI ([#73](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/73), [`8bbe3f2`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/8bbe3f24772efd720f28135f376171a37c2897d9)) ## v2.8.7 (2022-12-02) ### Bug Fixes - Enable service cache by default since esp32s are unreliable without it ([#71](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/71), [`0e90c1c`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/0e90c1c79fac01e5e0a39c51b733616d1d324aeb)) ## v2.8.6 (2022-11-30) ### Bug Fixes - Stop trying to check dbus once the socket is missing ([#70](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/70), [`74bd63b`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/74bd63b5b5c68eca6e7f0fa4e932a3ebab26a59e)) ## v2.8.5 (2022-11-19) ### Bug Fixes - Teach the connector about more esp32 errors and times ([#68](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/68), [`09cb73d`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/09cb73df4d6908665220df74f91aee4d200f6bad)) ## v2.8.4 (2022-11-11) ### Bug Fixes - Increase backoff when local ble adapter runs out of connection slots ([#67](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/67), [`cac7e57`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/cac7e57fbb13ee7beaa1eb18d51def661bc92ee3)) ## v2.8.3 (2022-11-06) ### Bug Fixes - Adjust connect timeout to match macos write timeout ([#66](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/66), [`1396fdc`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/1396fdc0b3235cf67ae919bf1c2a308d4437d023)) ## v2.8.2 (2022-11-01) ### Bug Fixes - Adjust backoffs for slower esp32 proxies ([#64](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/64), [`702a829`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/702a82921ad30fb4934b5056271cea842a758c08)) ## v2.8.1 (2022-10-31) ### Bug Fixes - Reduce logging as timeouts are expected ([#63](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/63), [`8b91838`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/8b918380d4772544e4471456df459f5d6d457a61)) ## v2.8.0 (2022-10-31) ### Features - Mark ESP_GATT_ERROR as a transient error ([#62](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/62), [`6d76ac4`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/6d76ac433c0d4727c12c4f7b4de0b039e7bbc4c2)) ## v2.7.0 (2022-10-30) ### Features - Log the adapter when connecting ([#61](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/61), [`ab873c8`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/ab873c83da6dd37cd4da3e4e61c3f6fc1ffa0c9f)) ## v2.6.0 (2022-10-30) ### Features - Teach the connector about transient esp32 errors ([#60](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/60), [`486fbbc`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/486fbbc13b9665fcdacf79f7240240602c8f477a)) ## v2.5.0 (2022-10-29) ### Features - Increase timeouts now that bleak has resolved the timeout with service discovery and bluez ([#59](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/59), [`2a65e27`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/2a65e276ffe3ab598eb8f6eb3cf3bcf7a5269780)) ## v2.4.2 (2022-10-24) ### Bug Fixes - Missing backoff execution with esp32 ([#58](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/58), [`3229424`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/3229424cae6dc7a9052efe080e110327eaa60f4d)) ## v2.4.1 (2022-10-24) ### Bug Fixes - Ensure we back off for longer when out of slots ([#57](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/57), [`efeced3`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/efeced3fa36fad7d0659e3ed30a7a150370dc923)) ## v2.4.0 (2022-10-24) ### Features - Improve handling of out of esp32 proxy connection slots ([#56](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/56), [`982b7ae`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/982b7ae1cc12d50a899329466fd4b760aaaec5ca)) ## v2.3.2 (2022-10-22) ### Bug Fixes - Ensure client is returned when debug is off ([#55](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/55), [`7ddcac8`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/7ddcac8f14126817cb5df4e7773739c6656dcd24)) ## v2.3.1 (2022-10-18) ### Bug Fixes - Do not attempt to disconnect non-bluez bledevices ([#54](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/54), [`54b6c84`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/54b6c8446629a216eeaf570f1677b67b38b6f081)) ## v2.3.0 (2022-10-15) ### Features - Add a retry_bluetooth_connection_error decorator ([#53](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/53), [`8bb706d`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/8bb706d09fa2cdaa3e2a3caf830dc92b26add4cc)) ## v2.2.0 (2022-10-15) ### Features - Update for new bleak 19 ([#52](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/52), [`9baafa5`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/9baafa5cbffa9fcd8ee8bd3040014c0d06a2085c)) ## v2.1.3 (2022-09-26) ### Bug Fixes - Bump dbus-fast ([#51](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/51), [`68167a3`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/68167a3eee222c3b0c241616c302aacacb8a3cdd)) ## v2.1.2 (2022-09-26) ### Bug Fixes - Adjust stale comment in freshen_ble_device ([#50](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/50), [`6cabc1f`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/6cabc1f557629c7591cb1cef482ae5d9791349b5)) ## v2.1.1 (2022-09-26) ### Bug Fixes - Set disconnected_callback in the constructor for newer bleak compat ([#49](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/49), [`e2e25b3`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/e2e25b3d6077aac7c766ecb844b0b492e66efff1)) ## v2.1.0 (2022-09-26) ### Features - Add get_device_by_adapter api ([#48](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/48), [`238b1f0`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/238b1f09b07e4e65dbf79472adbe9f7932f553fc)) ## v2.0.2 (2022-09-25) ### Bug Fixes - Republish to fix python-semantic-release detecting the wrong version ([#47](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/47), [`65f3cf2`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/65f3cf23dc2eab0f666dd45bd7b82058d32e2ba2)) - Republish to fix semantic-release detecting the wrong version ([#46](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/46), [`0338653`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/03386533e1d7fcb0c6d7ec0c34c085a356f6ecd0)) ### Features - Updates for bleak 0.18.0 ([#45](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/45), [`37b8729`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/37b872972f47216aa73a6e5b52ea7f9d5c116910)) BREAKING CHANGE: remove support for bleak < 0.18.0 ### Breaking Changes - Remove support for bleak < 0.18.0 ## v1.17.3 (2022-09-24) ### Bug Fixes - Log message when freshen fails ([#44](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/44), [`8365937`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/83659374d661c67b81ba204bda9d0f8bf886adf1)) ## v1.17.2 (2022-09-23) ### Bug Fixes - Add a guard to freshen_ble_device so it can be called on non-linux ([#43](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/43), [`4558a67`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/4558a67d4a291f6986dbc7d80fc1cfb2c9f4b4da)) ## v1.17.1 (2022-09-15) ### Bug Fixes - Adjust backoff times to reduce race risk ([#40](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/40), [`786b442`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/786b442c5e0102b6693cf98f86770a6ad80e4157)) ## v1.17.0 (2022-09-15) ### Features - Provide a BLEAK_RETRY_EXCEPTIONS constant ([#39](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/39), [`55dc2e1`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/55dc2e141d9059ed544ab4f0d333d09c97f6fab0)) ## v1.16.0 (2022-09-14) ### Features - Do not disconnect unexpectedly connected devices if bleak supports reusing them ([#35](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/35), [`be603ce`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/be603ce379f6a46ee750e7c3bcbd79d533d2a3ff)) Bleak 0.17 supports connecting to devices that are already connected in BlueZ. We now detect this and adjust the BLEDevice to point to the already connected device so they do not have to wait for a connection. This also fixes a race where the connection times out but the connection is actually made on the bus but we think it failed because we hit the timeout, so the next attempt will instead sail right though and be connected. ## v1.15.1 (2022-09-13) ### Bug Fixes - Revert requirement for newer bleak ([#34](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/34), [`fe7ec26`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/fe7ec26a7479855e6c37dd4f9b5ac86c93d8d1b8)) ## v1.15.0 (2022-09-12) ### Features - Bleak 0.17 support ([#33](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/33), [`ffce2c5`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/ffce2c51d3acddfa1efa9e2a396956521a768dd1)) ## v1.14.0 (2022-09-11) ### Features - Implement a smarter backoff ([#32](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/32), [`8272daa`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/8272daa12d3553001b53f637407a720ab209a57f)) ## v1.13.2 (2022-09-11) ### Bug Fixes - Race during disconnect when unexpectedly connected ([#30](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/30), [`2ceef9f`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/2ceef9f49cedb3f2721f5d969b4117fe2cb2de7c)) ## v1.13.1 (2022-09-11) ### Bug Fixes - Disconnect unexpectedly connected devices on other adapters ([#29](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/29), [`85a3efe`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/85a3efe1589dc48eb009da7c3aaa69d7decfe26d)) ## v1.13.0 (2022-09-10) ### Features - Make get_device and close_stale_connections part of __all__ ([#27](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/27), [`4d7edfd`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/4d7edfd2f2597e8cba96c925a1e7f4ae55986623)) ## v1.12.3 (2022-09-10) ### Bug Fixes - Disconnect devices that are unexpectedly connected before connecting ([#26](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/26), [`47b31d3`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/47b31d38b481288472a6923d968a9c4dd6f2b1c6)) ## v1.12.2 (2022-09-10) ### Bug Fixes - Handle already connected devices with no rssi value ([#25](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/25), [`0dfd3b0`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/0dfd3b07ae6836a61d31c613534e3322dabd3761)) ## v1.12.1 (2022-09-10) ### Bug Fixes - Get_device returning no device when already connected ([#24](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/24), [`1063b76`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/1063b764959cabfdab572de85d1ad622e6ff7a20)) ## v1.12.0 (2022-09-10) ### Features - Add get_device helper to find already connected devices ([#23](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/23), [`595e6a0`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/595e6a09c06a29a55000e6b582db12b85884f75a)) ## v1.11.1 (2022-09-10) ### Bug Fixes - Handle Dbus EOFError while connecting ([#22](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/22), [`b0bc92d`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/b0bc92d00b77f570836fa59f4f88403152f78539)) ## v1.11.0 (2022-08-20) ### Features - Handle stale BLEDevices when an adapter goes offline ([#21](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/21), [`012c94c`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/012c94c17e81511f84764037a48be1ba686453b3)) ## v1.10.1 (2022-08-19) ### Bug Fixes - Add workaround for when get_services raises ([#20](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/20), [`1c92f6e`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/1c92f6ed3b643f8f739e7a27b56111cd71e23696)) ## v1.10.0 (2022-08-19) ### Features - Log path to the device ([#19](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/19), [`6a9f293`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/6a9f2930e06ee393f0b3885c3d845d485c18babd)) ## v1.9.0 (2022-08-19) ### Features - Add ble_device_callback to get a new BLEDevice between connect attempts ([#18](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/18), [`450268b`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/450268b8498f730576957f0dbb1cbe0dedbdf14a)) ## v1.8.0 (2022-08-15) ### Features - Add last known rssi to the debug log ([#17](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/17), [`1032317`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/10323172a1faacca01e6bfb690e92b9e5fb1bd80)) ## v1.7.2 (2022-08-12) ### Bug Fixes - Handle device going in and out of range frequently ([#16](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/16), [`89b8c1b`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/89b8c1ba63151043d0d6977b0c4a173cb616a9a5)) ## v1.7.1 (2022-08-12) ### Bug Fixes - Race during disconnect error ([#14](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/14), [`dccbbb1`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/dccbbb1e34028dbd3e3b155502bb70d1ffaa11a8)) ## v1.7.0 (2022-08-11) ### Features - Add ble_device_has_changed helper ([#13](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/13), [`0a23bb8`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/0a23bb8bbd2c8d0fafc20f3d2da36415ed4759be)) ## v1.6.0 (2022-08-11) ### Features - Cached services ([#11](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/11), [`1fe23d6`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/1fe23d6397a7ac2b5994778a9ddc06e687de5ba3)) ## v1.5.0 (2022-08-08) ### Features - Rethrow UnknownObject as BleakNotFoundError ([#12](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/12), [`a07c50e`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/a07c50e8a910aadc6cec806c0e8888a00def97f6)) ## v1.4.0 (2022-08-05) ### Features - Improve error reporting when there is a poor connection ([#10](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/10), [`d022777`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/d0227773ff01b4d665fd7bd3e94a330d61214f88)) ## v1.3.0 (2022-08-04) ### Features - Improve chance of connecting with poor signal ([#9](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/9), [`f0322e7`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/f0322e73d450eaf0d088f0dd26a934f3fff40907)) ## v1.2.0 (2022-08-03) ### Features - Handle BrokenPipeError from dbus-next via bleak ([#8](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/8), [`21da55d`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/21da55dcc37754bcbf904c6ab8162cd4f091e2c4)) ## v1.1.1 (2022-08-02) ### Bug Fixes - Add back the bleak overall safety timeout ([#7](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/7), [`f3f8ded`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/f3f8ded4082bb155d2626a3ec3c693b11bbc355b)) ## v1.1.0 (2022-07-24) ### Features - Pass additional kwargs to the client class ([#6](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/6), [`808e05b`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/808e05bc2d831307f3e093a9d9d42a2409a0a681)) ## v1.0.2 (2022-07-22) ### Bug Fixes - Push a new release now that pypi is working again ([#5](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/5), [`3480e22`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/3480e225e8567e6b4a75d166c6a5b3e4661ebb46)) ## v1.0.1 (2022-07-22) ### Bug Fixes - Add comments ([#4](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/4), [`4bc5563`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/4bc5563c23bc8cdb9ae44ede0d2ea86693968610)) ## v1.0.0 (2022-07-22) ## v0.1.1 (2022-07-22) ### Bug Fixes - Republish ([#3](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/3), [`2b1a504`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/2b1a5042f2250db16d655b1f18a24e74f82f77d2)) ## v0.1.0 (2022-07-22) ### Features - First release ([#2](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/2), [`f11f9b5`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/f11f9b5ea1a998bfbd407ffaff299d40243e4e0a)) ## v0.0.1 (2022-07-22) ### Chores - Initial commit ([`7128f20`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/7128f2035025491075f84ca9f9b86306291dd1da)) ### Features - Init repo ([#1](https://github.com/Bluetooth-Devices/bleak-retry-connector/pull/1), [`ea99576`](https://github.com/Bluetooth-Devices/bleak-retry-connector/commit/ea99576e4ef2ae10ecfbcd067256d8476d1bf8de)) bleak-retry-connector-3.10.0/CONTRIBUTING.md000066400000000000000000000075021477303675200202570ustar00rootroot00000000000000# Contributing Contributions are welcome, and they are greatly appreciated! Every little helps, and credit will always be given. You can contribute in many ways: ## Types of Contributions ### Report Bugs Report bugs to [our issue page][gh-issues]. If you are reporting a bug, please include: - Your operating system name and version. - Any details about your local setup that might be helpful in troubleshooting. - Detailed steps to reproduce the bug. ### Fix Bugs Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. ### Implement Features Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. ### Write Documentation Bleak Retry Connector could always use more documentation, whether as part of the official Bleak Retry Connector docs, in docstrings, or even on the web in blog posts, articles, and such. ### Submit Feedback The best way to send feedback [our issue page][gh-issues] on GitHub. If you are proposing a feature: - Explain in detail how it would work. - Keep the scope as narrow as possible, to make it easier to implement. - Remember that this is a volunteer-driven project, and that contributions are welcome 😊 ## Get Started! Ready to contribute? Here's how to set yourself up for local development. 1. Fork the repo on GitHub. 2. Clone your fork locally: ```shell $ git clone git@github.com:your_name_here/bleak-retry-connector.git ``` 3. Install the project dependencies with [Poetry](https://python-poetry.org): ```shell $ poetry install ``` 4. Create a branch for local development: ```shell $ git checkout -b name-of-your-bugfix-or-feature ``` Now you can make your changes locally. 5. When you're done making changes, check that your changes pass our tests: ```shell $ poetry run pytest ``` 6. Linting is done through [pre-commit](https://pre-commit.com). Provided you have the tool installed globally, you can run them all as one-off: ```shell $ pre-commit run -a ``` Or better, install the hooks once and have them run automatically each time you commit: ```shell $ pre-commit install ``` 7. Commit your changes and push your branch to GitHub: ```shell $ git add . $ git commit -m "feat(something): your detailed description of your changes" $ git push origin name-of-your-bugfix-or-feature ``` Note: the commit message should follow [the conventional commits](https://www.conventionalcommits.org). We run [`commitlint` on CI](https://github.com/marketplace/actions/commit-linter) to validate it, and if you've installed pre-commit hooks at the previous step, the message will be checked at commit time. 8. Submit a pull request through the GitHub website or using the GitHub CLI (if you have it installed): ```shell $ gh pr create --fill ``` ## Pull Request Guidelines We like to have the pull request open as soon as possible, that's a great place to discuss any piece of work, even unfinished. You can use draft pull request if it's still a work in progress. Here are a few guidelines to follow: 1. Include tests for feature or bug fixes. 2. Update the documentation for significant features. 3. Ensure tests are passing on CI. ## Tips To run a subset of tests: ```shell $ pytest tests ``` ## Making a new release The deployment should be automated and can be triggered from the Semantic Release workflow in GitHub. The next version will be based on [the commit logs](https://python-semantic-release.readthedocs.io/en/latest/commit-log-parsing.html#commit-log-parsing). This is done by [python-semantic-release](https://python-semantic-release.readthedocs.io/en/latest/index.html) via a GitHub action. [gh-issues]: https://github.com/bluetooth-devices/bleak-retry-connector/issues bleak-retry-connector-3.10.0/LICENSE000066400000000000000000000020601477303675200170250ustar00rootroot00000000000000 MIT License Copyright (c) 2022 J. Nick Koston Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bleak-retry-connector-3.10.0/README.md000066400000000000000000000071371477303675200173110ustar00rootroot00000000000000# Bleak Retry Connector

CI Status Documentation Status Test coverage percentage

Poetry black pre-commit

PyPI Version Supported Python versions License

A connector for Bleak Clients that handles transient connection failures ## Installation Install this via pip (or your favourite package manager): `pip install bleak-retry-connector` ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! ## Credits This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the [browniebroke/cookiecutter-pypackage](https://github.com/browniebroke/cookiecutter-pypackage) project template. bleak-retry-connector-3.10.0/commitlint.config.mjs000066400000000000000000000003621477303675200221610ustar00rootroot00000000000000export default { extends: ["@commitlint/config-conventional"], rules: { "header-max-length": [0, "always", Infinity], "body-max-line-length": [0, "always", Infinity], "footer-max-line-length": [0, "always", Infinity], }, }; bleak-retry-connector-3.10.0/docs/000077500000000000000000000000001477303675200167525ustar00rootroot00000000000000bleak-retry-connector-3.10.0/docs/Makefile000066400000000000000000000011751477303675200204160ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) bleak-retry-connector-3.10.0/docs/make.bat000066400000000000000000000013741477303675200203640ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd bleak-retry-connector-3.10.0/docs/source/000077500000000000000000000000001477303675200202525ustar00rootroot00000000000000bleak-retry-connector-3.10.0/docs/source/_static/000077500000000000000000000000001477303675200217005ustar00rootroot00000000000000bleak-retry-connector-3.10.0/docs/source/_static/.gitkeep000066400000000000000000000000001477303675200233170ustar00rootroot00000000000000bleak-retry-connector-3.10.0/docs/source/changelog.md000066400000000000000000000000451477303675200225220ustar00rootroot00000000000000```{include} ../../CHANGELOG.md ``` bleak-retry-connector-3.10.0/docs/source/conf.py000066400000000000000000000036571477303675200215640ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) from typing import Any # -- Project information ----------------------------------------------------- project = "Bleak Retry Connector" copyright = "2020, J. Nick Koston" author = "J. Nick Koston" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "myst_parser", ] # The suffix of source filenames. source_suffix = [".rst", ".md"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns: list[Any] = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] bleak-retry-connector-3.10.0/docs/source/contributing.md000066400000000000000000000000501477303675200232760ustar00rootroot00000000000000```{include} ../../CONTRIBUTING.md ``` bleak-retry-connector-3.10.0/docs/source/index.md000066400000000000000000000003651477303675200217070ustar00rootroot00000000000000# Welcome to Bleak Retry Connector documentation! ```{toctree} :caption: Installation & Usage :maxdepth: 2 installation usage ``` ```{toctree} :caption: Project Info :maxdepth: 2 changelog contributing ``` ```{include} ../../README.md ``` bleak-retry-connector-3.10.0/docs/source/installation.md000066400000000000000000000003001477303675200232660ustar00rootroot00000000000000# Installation The package is published on [PyPI](https://pypi.org/project/deezer-python/) and can be installed with `pip` (or any equivalent): ```bash pip install bleak-retry-connector ``` bleak-retry-connector-3.10.0/docs/source/usage.md000066400000000000000000000001531477303675200216770ustar00rootroot00000000000000# Usage To use this package, import it: ```python import bleak_retry_connector ``` TODO: Document usage bleak-retry-connector-3.10.0/poetry.lock000066400000000000000000002034461477303675200202270ustar00rootroot00000000000000# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "aiooui" version = "0.1.9" description = "Async OUI lookups" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] markers = "python_version < \"3.14\" and platform_system == \"Linux\"" files = [ {file = "aiooui-0.1.9-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:64d904b43f14dd1d8d9fcf1684d9e2f558bc5e0bd68dc10023c93355c9027907"}, {file = "aiooui-0.1.9-py3-none-any.whl", hash = "sha256:737a5e62d8726540218c2b70e5f966d9912121e4644f3d490daf8f3c18b182e5"}, {file = "aiooui-0.1.9.tar.gz", hash = "sha256:e8c8bc59ab352419e0747628b4cce7c4e04d492574c1971e223401126389c5d8"}, ] [[package]] name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" groups = ["main"] markers = "python_version < \"3.11\"" files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [[package]] name = "bleak" version = "0.22.3" description = "Bluetooth Low Energy platform Agnostic Klient" optional = false python-versions = "<3.14,>=3.8" groups = ["main"] markers = "python_version < \"3.14\"" files = [ {file = "bleak-0.22.3-py3-none-any.whl", hash = "sha256:1e62a9f5e0c184826e6c906e341d8aca53793e4596eeaf4e0b191e7aca5c461c"}, {file = "bleak-0.22.3.tar.gz", hash = "sha256:3149c3c19657e457727aa53d9d6aeb89658495822cd240afd8aeca4dd09c045c"}, ] [package.dependencies] async-timeout = {version = ">=3.0.0,<5", markers = "python_version < \"3.11\""} bleak-winrt = {version = ">=1.2.0,<2.0.0", markers = "platform_system == \"Windows\" and python_version < \"3.12\""} dbus-fast = {version = ">=1.83.0,<3", markers = "platform_system == \"Linux\""} pyobjc-core = {version = ">=10.3,<11.0", markers = "platform_system == \"Darwin\""} pyobjc-framework-CoreBluetooth = {version = ">=10.3,<11.0", markers = "platform_system == \"Darwin\""} pyobjc-framework-libdispatch = {version = ">=10.3,<11.0", markers = "platform_system == \"Darwin\""} typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.12\""} winrt-runtime = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Devices.Bluetooth" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Devices.Bluetooth.Advertisement" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Devices.Enumeration" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Foundation" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Foundation.Collections" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} "winrt-Windows.Storage.Streams" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} [[package]] name = "bleak-winrt" version = "1.2.0" description = "Python WinRT bindings for Bleak" optional = false python-versions = "*" groups = ["main"] markers = "python_version < \"3.12\" and platform_system == \"Windows\"" 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 = "bluetooth-adapters" version = "0.21.4" description = "Tools to enumerate and find Bluetooth Adapters" optional = false python-versions = ">=3.9" groups = ["main"] markers = "python_version < \"3.14\" and platform_system == \"Linux\"" files = [ {file = "bluetooth_adapters-0.21.4-py3-none-any.whl", hash = "sha256:ce2e8139cc9d7b103c21654c6309507979e469aae3efebcaeee9923080b0569b"}, {file = "bluetooth_adapters-0.21.4.tar.gz", hash = "sha256:a5a809ef7ba95ee673a78704f90ce34612deb3696269d1a6fd61f98642b99dd3"}, ] [package.dependencies] aiooui = ">=0.1.1" async-timeout = {version = ">=3.0.0", markers = "python_version < \"3.11\""} bleak = ">=0.21.1" dbus-fast = {version = ">=1.21.0", markers = "platform_system == \"Linux\""} uart-devices = ">=0.1.0" usb-devices = ">=0.4.5" [package.extras] docs = ["Sphinx (>=5,<8)", "myst-parser (>=0.18,<3.1)", "sphinx-rtd-theme (>=1,<4)"] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "coverage" version = "7.8.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "dbus-fast" version = "2.43.0" description = "A faster version of dbus-next" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "dbus_fast-2.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:047aa3358c47c6317a65716f9cbdfc6b8f95309df6ef93e4923e16083f610d62"}, {file = "dbus_fast-2.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92447e68c316f2565d4961c3168aeb2939936acca20dc992121efd77966c1142"}, {file = "dbus_fast-2.43.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c361a721533f7b0ba82dc3d6193bcd21638c85d4500f1808f4ea3a8d91628d1d"}, {file = "dbus_fast-2.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cf0549696371e3829d2bee600d834bd9795a822b2629ad7295f6f4e1c266c3"}, {file = "dbus_fast-2.43.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:754f4f3ce0fb3c1aec554b693c2724f1ab15f31129429a4392a85879360d8abf"}, {file = "dbus_fast-2.43.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5a96a7ffedf1ae3a56c82167533052833a4dd858aa6a060e0d9ee9bbc79cb6d5"}, {file = "dbus_fast-2.43.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:06c015378d5f4f054be56e6e421575892e772962ac32974a254659a231880cd3"}, {file = "dbus_fast-2.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d21f3fb26e98da422c00903b620dbf9f8d9082aada278f7b23fd95c1d065034"}, {file = "dbus_fast-2.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:faf2d0e65af2a17cf12e2a6a5e17bf12012e557f83a8c3ab532935ea7d7230c1"}, {file = "dbus_fast-2.43.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:090355ef6da1710732dbbbe885ec8a36f4e37ab45eb110d77b92cc5d6e4a5682"}, {file = "dbus_fast-2.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753e2c7c0b15bf5159936c4bd9e70f15219b7d518f1e963af7caa7f84e495596"}, {file = "dbus_fast-2.43.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e9b48153edac93a67ed3013eb5acb03d9dd52be74b9ccde1421bf86d52f88266"}, {file = "dbus_fast-2.43.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:629002510e62af1956129e6d69a60a8fb812394e754a69bab90e254d6a03eabb"}, {file = "dbus_fast-2.43.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b0d32c2e8fe0ca8927b91683c9423d8cfdaf2c45d29027a4b24ea0e94a80c9b9"}, {file = "dbus_fast-2.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f0e20fb3d908cfc19ed0c8472d5323262a5b0fd96898c6eec5518638c19e8fe"}, {file = "dbus_fast-2.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e94023f7492ac9c0cd401291f9391308817163a32ab20f7962f67be1744d4347"}, {file = "dbus_fast-2.43.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:723573933212e281e7667bfa8944f4e983734bf0aa9340f23f904466646ee4d7"}, {file = "dbus_fast-2.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40473392d29b9c580c2e16471aa868c4477fc5b7c9af88bf08f66b36cf6b3087"}, {file = "dbus_fast-2.43.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e506846fc8b84588e37f734c7cda3f81a3d83a0ed93974391929fa1510ef3bdf"}, {file = "dbus_fast-2.43.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:06818da2bdd855120fa0ec65411182ab774f034dfad84ebfa26c576bbb570f44"}, {file = "dbus_fast-2.43.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b41abbd5b9c289a5251ef282664c122f420f210199d36e1a3a536e3b814bb551"}, {file = "dbus_fast-2.43.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f757d9a1e3b141f932daea37c657a61f83e08f2204267dd5194d446713038d27"}, {file = "dbus_fast-2.43.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66608a91fa4118fea8afcf6836fc358d54dfce7fe37c740a9092adc3069490f1"}, {file = "dbus_fast-2.43.0-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b0b54bfd87f42c034c998f5b56806a30c9f5f6ea999ba575438cd48be321bb5c"}, {file = "dbus_fast-2.43.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:067d63faf2714d129629b1e705dae89bcf1c8cb9ac5e741882d44b8323975326"}, {file = "dbus_fast-2.43.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:c184f2cc52994845d0a88011985a12562a1433332f79270246c7dc667a323024"}, {file = "dbus_fast-2.43.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eddff9abbc50be47f7b618577a2d3f475fcf7ad96e8dc3549cee29c985ec018e"}, {file = "dbus_fast-2.43.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a5533f40df3d2dd111eba9752d7d7b716bad214a4ff443fd7d5ab1bd61cea18b"}, {file = "dbus_fast-2.43.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:09e836c7453cf0dab8f6ae118c17ef431d8cfc8fc88bd2e725c1f1d4c5e85e6b"}, {file = "dbus_fast-2.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1996a1081ddc58072ef6c5435a401fe2733dbe115296001a6adce0cd868bb479"}, {file = "dbus_fast-2.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00be0f9a6edf04dc89c4ef774cff191986cb9749f9316efc08e6d8f725eeb97a"}, {file = "dbus_fast-2.43.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:451fb0913f52bf59ca091d8ed5d0dbc62fe9a1d8e78102143cf29c21b922b72f"}, {file = "dbus_fast-2.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6c81bcea572fcd672c4d95c2bc18ec932b41166365bb7463fa9da3b57d22aa2"}, {file = "dbus_fast-2.43.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f9253254789b6186f7b5f1bba49bb8ebdc8274cfec6ee63e271aba1390cc15f"}, {file = "dbus_fast-2.43.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ce8f2e641c4e6b521e3c6820e155952b35a4eeb0c483d7ce6f1cb22761483ee0"}, {file = "dbus_fast-2.43.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f3c98520cabb443b2cf53f3a11fea1f7b92dcca62d628254effb46760bb420a2"}, {file = "dbus_fast-2.43.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9bccb077f3640ee4ed2ed7aa62811730fda0bfd25b27078d0715697f54ee6122"}, {file = "dbus_fast-2.43.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16338110aeb588684baa662dcf4eaf904a493a2417498b188f9080abc36c481e"}, {file = "dbus_fast-2.43.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:92f5117d8910d82804226088cbbce68d3180926421b4dfb7478dccac14329706"}, {file = "dbus_fast-2.43.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d88ffb7f7bb3db79758d4b0fd80afb608ae113b8d12b7045d3b573118e2a5f"}, {file = "dbus_fast-2.43.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:467da011482b6b6e3af3c34974dd99e28a025ea93b3cc050c2e91a2e8470de5a"}, {file = "dbus_fast-2.43.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f71c50d9e108ce5f881356f20be47595bc2dcf44335902d21eeb4162e6f1dd9"}, {file = "dbus_fast-2.43.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9683d2db4718f743553302f996b1df978d07eead38b8a7c18a19d704bf1a82de"}, {file = "dbus_fast-2.43.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ce35380a6f3db2406b31b0744a262a5f348124a586df8cc2f8417286ab1984d"}, {file = "dbus_fast-2.43.0.tar.gz", hash = "sha256:c4968dd6e8c15c27a307a0b3c90347ab1eb7a0787d189e65896d404eb1c0728f"}, ] [[package]] name = "exceptiongroup" version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "iniconfig" version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] name = "packaging" version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pyobjc-core" version = "10.3.2" description = "Python<->ObjC Interoperability Module" optional = false python-versions = ">=3.8" groups = ["main"] markers = "python_version < \"3.14\" and platform_system == \"Darwin\"" files = [ {file = "pyobjc_core-10.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:acb40672d682851a5c7fd84e5041c4d069b62076168d72591abb5fcc871bb039"}, {file = "pyobjc_core-10.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cea5e77659619ad93c782ca07644b6efe7d7ec6f59e46128843a0a87c1af511a"}, {file = "pyobjc_core-10.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:16644a92fb9661de841ba6115e5354db06a1d193a5e239046e840013c7b3874d"}, {file = "pyobjc_core-10.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:76b8b911d94501dac89821df349b1860bb770dce102a1a293f524b5b09dd9462"}, {file = "pyobjc_core-10.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8c6288fdb210b64115760a4504efbc4daffdc390d309e9318eb0e3e3b78d2828"}, {file = "pyobjc_core-10.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:87901e9f7032f33eb4fa884e407bf2744d5a0791b379bfca783982a02be3f7fb"}, {file = "pyobjc_core-10.3.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:636971ab48a4198ca129e149fe58ccf85a7b4a9b93d27f5ae920d88eb2655431"}, {file = "pyobjc_core-10.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:48e9ac3af42b2340dae709a8b894f5ef7e5132d8546adcd1797cffcc449dabdc"}, {file = "pyobjc_core-10.3.2.tar.gz", hash = "sha256:dbf1475d864ce594288ce03e94e3a98dc7f0e4639971eb1e312bdf6661c21e0e"}, ] [[package]] name = "pyobjc-framework-cocoa" version = "10.3.2" description = "Wrappers for the Cocoa frameworks on macOS" optional = false python-versions = ">=3.8" groups = ["main"] markers = "python_version < \"3.14\" and platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_Cocoa-10.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:61f44c2adab28fdf3aa3d593c9497a2d9ceb9583ed9814adb48828c385d83ff4"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7caaf8b260e81b27b7b787332846f644b9423bfc1536f6ec24edbde59ab77a87"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c49e99fc4b9e613fb308651b99d52a8a9ae9916c8ef27aa2f5d585b6678a59bf"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1161b5713f9b9934c12649d73a6749617172e240f9431eff9e22175262fdfda"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:08e48b9ee4eb393447b2b781d16663b954bd10a26927df74f92e924c05568d89"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7faa448d2038ae0e0287a326d390002e744bb6470e45995e2dbd16c892e4495a"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:fcd53fee2be9708576617994b107aedc2c40824b648cd51e780e8399c0a447b6"}, {file = "pyobjc_framework_Cocoa-10.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:838fcf0d10674bde9ff64a3f20c0e188f2dc5e804476d80509b81c4ac1dabc59"}, {file = "pyobjc_framework_cocoa-10.3.2.tar.gz", hash = "sha256:673968e5435845bef969bfe374f31a1a6dc660c98608d2b84d5cae6eafa5c39d"}, ] [package.dependencies] pyobjc-core = ">=10.3.2" [[package]] name = "pyobjc-framework-corebluetooth" version = "10.3.2" description = "Wrappers for the framework CoreBluetooth on macOS" optional = false python-versions = ">=3.8" groups = ["main"] markers = "python_version < \"3.14\" and platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_CoreBluetooth-10.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:af3e2f935a6a7e5b009b4cf63c64899592a7b46c3ddcbc8f2e28848842ef65f4"}, {file = "pyobjc_framework_CoreBluetooth-10.3.2-cp36-abi3-macosx_10_13_universal2.whl", hash = "sha256:973b78f47c7e2209a475e60bcc7d1b4a87be6645d39b4e8290ee82640e1cc364"}, {file = "pyobjc_framework_CoreBluetooth-10.3.2-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:4bafdf1be15eae48a4878dbbf1bf19877ce28cbbba5baa0267a9564719ee736e"}, {file = "pyobjc_framework_CoreBluetooth-10.3.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:4d7dc7494de66c850bda7b173579df7481dc97046fa229d480fe9bf90b2b9651"}, {file = "pyobjc_framework_CoreBluetooth-10.3.2-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:62e09e730f4d98384f1b6d44718812195602b3c82d5c78e09f60e8a934e7b266"}, {file = "pyobjc_framework_corebluetooth-10.3.2.tar.gz", hash = "sha256:c0a077bc3a2466271efa382c1e024630bc43cc6f9ab8f3f97431ad08b1ad52bb"}, ] [package.dependencies] pyobjc-core = ">=10.3.2" pyobjc-framework-Cocoa = ">=10.3.2" [[package]] name = "pyobjc-framework-libdispatch" version = "10.3.2" description = "Wrappers for libdispatch on macOS" optional = false python-versions = ">=3.8" groups = ["main"] markers = "python_version < \"3.14\" and platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_libdispatch-10.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35233a8b1135567c7696087f924e398799467c7f129200b559e8e4fa777af860"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:061f6aa0f88d11d993e6546ec734303cb8979f40ae0f5cd23541236a6b426abd"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6bb528f34538f35e1b79d839dbfc398dd426990e190d9301fe2d811fddc3da62"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1357729d5fded08fbf746834ebeef27bee07d6acb991f3b8366e8f4319d882c4"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:210398f9e1815ceeff49b578bf51c2d6a4a30d4c33f573da322f3d7da1add121"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e7ae5988ac0b369ad40ce5497af71864fac45c289fa52671009b427f03d6871f"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:f9d51d52dff453a4b19c096171a6cd31dd5e665371c00c1d72d480e1c22cd3d4"}, {file = "pyobjc_framework_libdispatch-10.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ef755bcabff2ea8db45603a8294818e0eeae85bf0b7b9d59e42f5947a26e33b9"}, {file = "pyobjc_framework_libdispatch-10.3.2.tar.gz", hash = "sha256:e9f4311fbf8df602852557a98d2a64f37a9d363acf4d75634120251bbc7b7304"}, ] [package.dependencies] pyobjc-core = ">=10.3.2" pyobjc-framework-Cocoa = ">=10.3.2" [[package]] name = "pytest" version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "0.25.3" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"}, {file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"}, ] [package.dependencies] pytest = ">=8.2,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" version = "6.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_cov-6.1.0-py3-none-any.whl", hash = "sha256:cd7e1d54981d5185ef2b8d64b50172ce97e6f357e6df5cb103e828c7f993e201"}, {file = "pytest_cov-6.1.0.tar.gz", hash = "sha256:ec55e828c66755e5b74a21bd7cc03c303a9f928389c0563e50ba454a6dbe71db"}, ] [package.dependencies] coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "tomli" version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_full_version <= \"3.11.0a6\"" 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"}, {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]] name = "typing-extensions" version = "4.13.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main"] markers = "python_version < \"3.12\"" files = [ {file = "typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5"}, {file = "typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b"}, ] [[package]] name = "uart-devices" version = "0.1.1" description = "UART Devices for Linux" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] markers = "python_version < \"3.14\" and platform_system == \"Linux\"" files = [ {file = "uart_devices-0.1.1-py3-none-any.whl", hash = "sha256:55bc8cce66465e90b298f0910e5c496bc7be021341c5455954cf61c6253dc123"}, {file = "uart_devices-0.1.1.tar.gz", hash = "sha256:3a52c4ae0f5f7400ebe1ae5f6e2a2d40cc0b7f18a50e895236535c4e53c6ed34"}, ] [[package]] name = "usb-devices" version = "0.4.5" description = "Tools for mapping, describing, and resetting USB devices" optional = false python-versions = ">=3.9,<4.0" groups = ["main"] markers = "python_version < \"3.14\" and platform_system == \"Linux\"" files = [ {file = "usb_devices-0.4.5-py3-none-any.whl", hash = "sha256:8a415219ef1395e25aa0bddcad484c88edf9673acdeae8a07223ca7222a01dcf"}, {file = "usb_devices-0.4.5.tar.gz", hash = "sha256:9b5c7606df2bc791c6c45b7f76244a0cbed83cb6fa4c68791a143c03345e195d"}, ] [[package]] name = "winrt-runtime" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" files = [ {file = "winrt_runtime-2.3.0-cp310-cp310-win32.whl", hash = "sha256:5c22ed339b420a6026134e28281b25078a9e6755eceb494dce5d42ee5814e3fd"}, {file = "winrt_runtime-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f3ef0d6b281a8d4155ea14a0f917faf82a004d4996d07beb2b3d2af191503fb1"}, {file = "winrt_runtime-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:93ce23df52396ed89dfe659ee0e1a968928e526b9c577942d4a54ad55b333644"}, {file = "winrt_runtime-2.3.0-cp311-cp311-win32.whl", hash = "sha256:352d70864846fd7ec89703845b82a35cef73f42d178a02a4635a38df5a61c0f8"}, {file = "winrt_runtime-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:286e6036af4903dd830398103c3edd110a46432347e8a52ba416d937c0e1f5f9"}, {file = "winrt_runtime-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:44d0f0f48f2f10c02b885989e8bbac41d7bf9c03550b20ddf562100356fca7a9"}, {file = "winrt_runtime-2.3.0-cp312-cp312-win32.whl", hash = "sha256:03d3e4aedc65832e57c0dbf210ec2a9d7fb2819c74d420ba889b323e9fa5cf28"}, {file = "winrt_runtime-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:0dc636aec2f4ee6c3849fa59dae10c128f4a908f0ce452e91af65d812ea66dcb"}, {file = "winrt_runtime-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:d9f140c71e4f3bf7bf7d6853b246eab2e1632c72f218ff163aa41a74b576736f"}, {file = "winrt_runtime-2.3.0-cp313-cp313-win32.whl", hash = "sha256:77f06df6b7a6cb536913ae455e30c1733d31d88dafe2c3cd8c3d0e2bcf7e2a20"}, {file = "winrt_runtime-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:7388774b74ea2f4510ab3a98c95af296665ebe69d9d7e2fd7ee2c3fc5856099e"}, {file = "winrt_runtime-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:0d3a4ac7661cad492d51653054e63328b940a6083c1ee1dd977f90069cb8afaa"}, {file = "winrt_runtime-2.3.0-cp39-cp39-win32.whl", hash = "sha256:cd7bce2c7703054e7f64d11be665e9728e15d9dae0d952a51228fe830e0c4b55"}, {file = "winrt_runtime-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:2da01af378ab9374a3a933da97543f471a676a3b844318316869bffeff811e8a"}, {file = "winrt_runtime-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:1c6bbfcc7cbe1c8159ed5d776b30b7f1cbc2c6990803292823b0788c22d75636"}, {file = "winrt_runtime-2.3.0.tar.gz", hash = "sha256:bb895a2b8c74b375781302215e2661914369c625aa1f8df84f8d37691b22db77"}, ] [[package]] name = "winrt-windows-devices-bluetooth" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp310-cp310-win32.whl", hash = "sha256:554aa6d0ca4bebc22a45f19fa60db1183a2b5643468f3c95cf0ebc33fbc1b0d0"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:cec2682e10431f027c1823647772671fb09bebc1e8a00021a3651120b846d36f"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:b4d42faef99845de2aded4c75c906f03cc3ba3df51fb4435e4cc88a19168cf99"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp311-cp311-win32.whl", hash = "sha256:64e0992175d4d5a1160179a8c586c2202a0edbd47a5b6da4efdbc8bb601f2f99"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:0830111c077508b599062fbe2d817203e4efa3605bd209cf4a3e03388ec39dda"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:3943d538cb7b6bde3fd8741591eb6e23487ee9ee6284f05428b205e7d10b6d92"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp312-cp312-win32.whl", hash = "sha256:544ed169039e6d5e250323cc18c87967cfeb4d3d09ce354fd7c5fd2283f3bb98"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:f7becf095bf9bc999629fcb6401a88b879c3531b3c55c820e63259c955ddc06c"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:a6a2980409c855b4e5dab0be9bde9f30236292ac1fc994df959fa5a518cd6690"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp313-cp313-win32.whl", hash = "sha256:82f443be43379d4762e72633047c82843c873b6f26428a18855ca7b53e1958d7"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:8b407da87ab52315c2d562a75d824dcafcae6e1628031cdb971072a47eb78ff0"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:e36d0b487bc5b64662b8470085edf8bfa5a220d7afc4f2e8d7faa3e3ac2bae80"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp39-cp39-win32.whl", hash = "sha256:6553023433edf5a75767e8962bf492d0623036975c7d8373d5bbccc633a77bbc"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:77bdeadb043190c40ebbad462cd06e38b6461bc976bc67daf587e9395c387aae"}, {file = "winrt_Windows.Devices.Bluetooth-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:c588ab79b534fedecce48f7082b419315e8d797d0120556166492e603e90d932"}, {file = "winrt_windows_devices_bluetooth-2.3.0.tar.gz", hash = "sha256:a1204b71c369a0399ec15d9a7b7c67990dd74504e486b839bf81825bd381a837"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (==2.3.0)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (==2.3.0)", "winrt-Windows.Devices.Enumeration[all] (==2.3.0)", "winrt-Windows.Devices.Radios[all] (==2.3.0)", "winrt-Windows.Foundation.Collections[all] (==2.3.0)", "winrt-Windows.Foundation[all] (==2.3.0)", "winrt-Windows.Networking[all] (==2.3.0)", "winrt-Windows.Storage.Streams[all] (==2.3.0)"] [[package]] name = "winrt-windows-devices-bluetooth-advertisement" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp310-cp310-win32.whl", hash = "sha256:4386498e7794ed383542ea868f0aa2dd8fb5f09f12bdffde024d12bd9f5a3756"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6fa25b2541d2898ae17982e86e0977a639b04f75119612cb46e1719474513fd"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:b200ff5acd181353f61f5b6446176faf78a61867d8c1d21e77a15e239d2cdf6b"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp311-cp311-win32.whl", hash = "sha256:e56ad277813b48e35a3074f286c55a7a25884676e23ef9c3fc12349a42cb8fa4"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d6533fef6a5914dc8d519b83b1841becf6fd2f37163d6e07df318a6a6118f194"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:8f4369cb0108f8ee0cace559f9870b00a4dde3fc1abd52f84adba08bc733825c"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp312-cp312-win32.whl", hash = "sha256:d729d989acd7c1d703e2088299b6e219089a415db4a7b80cd52fdc507ec3ce95"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d3d258d4388a2b46f2e46f2fbdede1bf327eaa9c2dd4605f8a7fe454077c49e"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:d8c12457b00a79f8f1058d7a51bd8e7f177fb66e31389469e75b1104f6358921"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp313-cp313-win32.whl", hash = "sha256:ac1e55a350881f82cb51e162cb7a4b5d9359e9e5fbde922de802404a951d64ec"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0fc339340fb8be21c1c829816a49dc31b986c6d602d113d4a49ee8ffaf0e2396"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:da63d9c56edcb3b2d5135e65cc8c9c4658344dd480a8a2daf45beb2106f17874"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp39-cp39-win32.whl", hash = "sha256:e98c6ae4b0afd3e4f3ab4fa06e84d6017ff9242146a64e3bad73f7f34183a076"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdc485f4143fbbb3ae0c9c9ad03b1021a5cb233c6df65bf56ac14f8e22c918c3"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:7af519cc895be84d6974e9f70d102545a5e8db05e065903b0fd84521218e60a9"}, {file = "winrt_windows_devices_bluetooth_advertisement-2.3.0.tar.gz", hash = "sha256:c8adbec690b765ca70337c35efec9910b0937a40a0a242184ea295367137f81c"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth[all] (==2.3.0)", "winrt-Windows.Foundation.Collections[all] (==2.3.0)", "winrt-Windows.Foundation[all] (==2.3.0)", "winrt-Windows.Storage.Streams[all] (==2.3.0)"] [[package]] name = "winrt-windows-devices-bluetooth-genericattributeprofile" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp310-cp310-win32.whl", hash = "sha256:1ec75b107370827874d8435a47852d0459cb66d5694e02a833e0a75c4748e847"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:0a178aa936abbc56ae1cc54a222dee4a34ce6c09506a5b592d4f7d04dbe76b95"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:b7067b8578e19ad17b28694090d5b000fee57db5b219462155961b685d71fba5"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp311-cp311-win32.whl", hash = "sha256:e0aeba201e20b6c4bc18a4336b5b07d653d4ab4c9c17a301613db680a346cd5e"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:f87b3995de18b98075ec2b02afc7252873fa75e7c840eb770d7bfafb4fda5c12"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:7dccce04ec076666001efca8e2484d0ec444b2302ae150ef184aa253b8cfba09"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp312-cp312-win32.whl", hash = "sha256:1b97ef2ab9c9f5bae984989a47565d0d19c84969d74982a2664a4a3485cb8274"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:5fac2c7b301fa70e105785d7504176c76e4d824fc3823afed4d1ab6a7682272c"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:353fdccf2398b2a12e0835834cff8143a7efd9ba877fb5820fdcce531732b500"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp313-cp313-win32.whl", hash = "sha256:f414f793767ccc56d055b1c74830efb51fa4cbdc9163847b1a38b1ee35778f49"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ef35d9cda5bbdcc55aa7eaf143ab873227d6ee467aaf28edbd2428f229e7c94"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a9e7308ba264175c2a9ee31f6cf1d647cb35ee9a1da7350793d8fe033a6b9b8"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp39-cp39-win32.whl", hash = "sha256:aea58f7e484cf3480ab9472a3e99b61c157b8a47baae8694bc7400ea5335f5dc"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:992b792a9e7f5771ccdc18eec4e526a11f23b75d9be5de3ec552ff719333897a"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:66b030a9cc6099dafe4253239e8e625cc063bb9bb115bebed6260d92dd86f6b1"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-2.3.0.tar.gz", hash = "sha256:f40f94bf2f7243848dc10e39cfde76c9044727a05e7e5dfb8cb7f062f3fd3dda"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth[all] (==2.3.0)", "winrt-Windows.Devices.Enumeration[all] (==2.3.0)", "winrt-Windows.Foundation.Collections[all] (==2.3.0)", "winrt-Windows.Foundation[all] (==2.3.0)", "winrt-Windows.Storage.Streams[all] (==2.3.0)"] [[package]] name = "winrt-windows-devices-enumeration" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp310-cp310-win32.whl", hash = "sha256:461360ab47967f39721e71276fdcfe87ad2f71ba7b09d721f2f88bcdf16a6924"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:a7d7b01d43d5dcc1f3846db12f4c552155efae75469f36052623faed7f0f74a8"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:6478fbe6f45172a9911c15b061ec9b0f30c9f4845ba3fd1e9e1bb78c1fb691c4"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp311-cp311-win32.whl", hash = "sha256:30be5cba8e9e81ea8dd514ba1300b5bb14ad7cc4e32efe908ddddd14c73e7f61"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86c2a1865e0a0146dd4f51f17e3d773d3e6732742f61838c05061f28738c6dbd"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:1b50d9304e49a9f04bc8139831b75be968ff19a1f50529d5eb0081dae2103d92"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp312-cp312-win32.whl", hash = "sha256:42ed0349f0290a1b0a101425a06196c5d5db1240db6f8bd7d2204f23c48d727b"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:83e385fbf85b9511699d33c659673611f42b98bd3a554a85b377a34cc3b68b2e"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:26f855caee61c12449c6b07e22ea1ad470f8daa24223d8581e1fe622c70b48a8"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp313-cp313-win32.whl", hash = "sha256:a5f2cff6ee584e5627a2246bdbcd1b3a3fd1e7ae0741f62c59f7d5a5650d5791"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:7516171521aa383ccdc8f422cc202979a2359d0d1256f22852bfb0b55d9154f0"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:80d01dfffe4b548439242f3f7a737189354768b203cca023dc29b267dfe5595a"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp39-cp39-win32.whl", hash = "sha256:990a375cd8edc2d30b939a49dcc1349ede3a4b8e4da78baf0de5e5711d3a4f00"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7bedf0eac2066d7d37b1d34071b95bb57024e9e083867be1d24e916e012ac0"}, {file = "winrt_Windows.Devices.Enumeration-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:c53b673b80ba794f1c1320a5e0a14d795193c3f64b8132ebafba2f49c7301c2f"}, {file = "winrt_windows_devices_enumeration-2.3.0.tar.gz", hash = "sha256:a14078aac41432781acb0c950fcdcdeb096e2f80f7591a3d46435f30221fc3eb"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.ApplicationModel.Background[all] (==2.3.0)", "winrt-Windows.Foundation.Collections[all] (==2.3.0)", "winrt-Windows.Foundation[all] (==2.3.0)", "winrt-Windows.Security.Credentials[all] (==2.3.0)", "winrt-Windows.Storage.Streams[all] (==2.3.0)", "winrt-Windows.UI.Popups[all] (==2.3.0)", "winrt-Windows.UI[all] (==2.3.0)"] [[package]] name = "winrt-windows-foundation" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Foundation-2.3.0-cp310-cp310-win32.whl", hash = "sha256:ea7b0e82be5c05690fedaf0dac5aa5e5fefd7ebf90b1497e5993197d305d916d"}, {file = "winrt_Windows.Foundation-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:6807dd40f8ecd6403679f6eae0db81674fdcf33768d08fdee66e0a17b7a02515"}, {file = "winrt_Windows.Foundation-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:0a861815e97ace82583210c03cf800507b0c3a97edd914bfffa5f88de1fbafcc"}, {file = "winrt_Windows.Foundation-2.3.0-cp311-cp311-win32.whl", hash = "sha256:c79b3d9384128b6b28c2483b4600f15c5d32c1f6646f9d77fdb3ee9bbaef6f81"}, {file = "winrt_Windows.Foundation-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:fdd9c4914070dc598f5961d9c7571dd7d745f5cc60347603bf39d6ee921bd85c"}, {file = "winrt_Windows.Foundation-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:62bbb0ffa273551d33fd533d6e09b6f9f633dc214225d483722af47d2525fb84"}, {file = "winrt_Windows.Foundation-2.3.0-cp312-cp312-win32.whl", hash = "sha256:d36f472ac258e79eee6061e1bb4ce50bfd200f9271392d23479c800ca6aee8d1"}, {file = "winrt_Windows.Foundation-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8de9b5e95a3fdabdb45b1952e05355dd5a678f80bf09a54d9f966dccc805b383"}, {file = "winrt_Windows.Foundation-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:37da09c08c9c772baedb1958e5ee116fe63809f33c6820c69750f340b3dda292"}, {file = "winrt_Windows.Foundation-2.3.0-cp313-cp313-win32.whl", hash = "sha256:2b00fad3f2a3859ccae41eee12ab44434813a371c2f3003b4f2419e5eecb4832"}, {file = "winrt_Windows.Foundation-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:686619932b2a2c689cbebc7f5196437a45fd2056656ef130bb10240bb111086a"}, {file = "winrt_Windows.Foundation-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b38dcb83fe82a7da9a57d7d5ad5deb09503b5be6d9357a9fd3016ca31673805d"}, {file = "winrt_Windows.Foundation-2.3.0-cp39-cp39-win32.whl", hash = "sha256:2d6922de4dc38061b86d314c7319d7c6bd78a52d64ee0c93eb81474bddb499bc"}, {file = "winrt_Windows.Foundation-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:1513e43adff3779d2f611d8bdf9350ac1a7c04389e9e6b1d777c5cd54f46e4fc"}, {file = "winrt_Windows.Foundation-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:c811e4a4f79b947fbbb50f74d34ef6840dd2dd26e0199bd61a4185e48c6a84a8"}, {file = "winrt_windows_foundation-2.3.0.tar.gz", hash = "sha256:c5766f011c8debbe89b460af4a97d026ca252144e62d7278c9c79c5581ea0c02"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (==2.3.0)"] [[package]] name = "winrt-windows-foundation-collections" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Foundation.Collections-2.3.0-cp310-cp310-win32.whl", hash = "sha256:d2fca59eef9582a33c2797b1fda1d5757d66827cc34e6fc1d1c94a5875c4c043"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d14b47d9137aebad71aa4fde5892673f2fa326f5f4799378cb9f6158b07a9824"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:cca5398a4522dffd76decf64a28368cda67e81dc01cad35a9f39cc351af69bdd"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp311-cp311-win32.whl", hash = "sha256:3808af64c95a9b464e8e97f6bec57a8b22168185f1c893f30de69aaf48c85b17"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1e9a3842a39feb965545124abfe79ed726adc5a1fc6a192470a3c5d3ec3f7a74"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:751c2a68fef080dfe0af892ef4cebf317844e4baa786e979028757fe2740fba4"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp312-cp312-win32.whl", hash = "sha256:498c1fc403d3dc7a091aaac92af471615de4f9550d544347cb3b169c197183b5"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:4d1b1cacc159f38d8e6b662f6e7a5c41879a36aa7434c1580d7f948c9037419e"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:398d93b76a2cf70d5e75c1f802e1dd856501e63bc9a31f4510ac59f718951b9e"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp313-cp313-win32.whl", hash = "sha256:1e5f1637e0919c7bb5b11ba1eebbd43bc0ad9600cf887b59fcece0f8a6c0eac3"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:c809a70bc0f93d53c7289a0a86d8869740e09fff0c57318a14401f5c17e0b912"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:269942fe86af06293a2676c8b2dcd5cb1d8ddfe1b5244f11c16e48ae0a5d100f"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp39-cp39-win32.whl", hash = "sha256:936b1c5720b564ec699673198addee97f3bdb790622d24c8fd1b346a9767717c"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:905a6ac9cd6b51659a9bba08cf44cfc925f528ef34cdd9c3a6c2632e97804a96"}, {file = "winrt_Windows.Foundation.Collections-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:1d6eac85976bd831e1b8cc479d7f14afa51c27cec5a38e2540077d3400cbd3ef"}, {file = "winrt_windows_foundation_collections-2.3.0.tar.gz", hash = "sha256:15c997fd6b64ef0400a619319ea3c6851c9c24e31d51b6448ba9bac3616d25a0"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Foundation[all] (==2.3.0)"] [[package]] name = "winrt-windows-storage-streams" version = "2.3.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" groups = ["main"] markers = "python_version >= \"3.12\" and python_version < \"3.14\" and platform_system == \"Windows\"" files = [ {file = "winrt_Windows.Storage.Streams-2.3.0-cp310-cp310-win32.whl", hash = "sha256:2c0901aee1232e92ed9320644b853d7801a0bdb87790164d56e961cd39910f07"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba07dc25decffd29aa8603119629c167bd03fa274099e3bad331a4920c292b78"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:5b60b48460095c50a00a6f7f9b3b780f5bdcb1ec663fc09458201499f93e23ea"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp311-cp311-win32.whl", hash = "sha256:8388f37759df64ceef1423ae7dd9275c8a6eb3b8245d400173b4916adc94b5ad"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:e5783dbe3694cc3deda594256ebb1088655386959bb834a6bfb7cd763ee87631"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:0a487d19c73b82aafa3d5ef889bb35e6e8e2487ca4f16f5446f2445033d5219c"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp312-cp312-win32.whl", hash = "sha256:272e87e6c74cb2832261ab33db7966a99e7a2400240cc4f8bf526a80ca054c68"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:997bf1a2d52c5f104b172947e571f27d9916a4409b4da592ec3e7f907848dd1a"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:d56daa00205c24ede6669d41eb70d6017e0202371d99f8ee2b0b31350ab59bd5"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp313-cp313-win32.whl", hash = "sha256:7ac4e46fc5e21d8badc5d41779273c3f5e7196f1cf2df1959b6b70eca1d5d85f"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1460027c94c107fcee484997494f3a400f08ee40396f010facb0e72b3b74c457"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:e4553a70f5264a7733596802a2991e2414cdcd5e396b9d11ee87be9abae9329e"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp39-cp39-win32.whl", hash = "sha256:28e1117e23046e499831af16d11f5e61e6066ed6247ef58b93738702522c29b0"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:5511dc578f92eb303aee4d3345ee4ffc88aa414564e43e0e3d84ff29427068f0"}, {file = "winrt_Windows.Storage.Streams-2.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6f5b3f8af4df08f5bf9329373949236ffaef22d021070278795e56da5326a876"}, {file = "winrt_windows_storage_streams-2.3.0.tar.gz", hash = "sha256:d2c010beeb1dd7c135ed67ecfaea13440474a7c469e2e9aa2852db27d2063d44"}, ] [package.dependencies] winrt-runtime = "2.3.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (==2.3.0)", "winrt-Windows.Foundation[all] (==2.3.0)", "winrt-Windows.Storage[all] (==2.3.0)", "winrt-Windows.System[all] (==2.3.0)"] [metadata] lock-version = "2.1" python-versions = ">=3.10" content-hash = "6f621c110839165527bc647f56934eacdf48d9d4a2e7704d99427ddf3a8b6eb7" bleak-retry-connector-3.10.0/pyproject.toml000066400000000000000000000050341477303675200207400ustar00rootroot00000000000000[project] name = "bleak-retry-connector" version = "3.10.0" description = "A connector for Bleak Clients that handles transient connection failures" authors = [{ name = "J. Nick Koston", email = "nick@koston.org" }] license = "MIT" readme = "README.md" requires-python = ">=3.10" dynamic = ["classifiers", "dependencies"] [project.urls] "Documentation" = "https://bleak-retry-connector.readthedocs.io" "Repository" = "https://github.com/bluetooth-devices/bleak-retry-connector" "Bug Tracker" = "https://github.com/bluetooth-devices/bleak-retry-connector/issues" "Changelog" = "https://github.com/bluetooth-devices/bleak-retry-connector/blob/main/CHANGELOG.md" [tool.poetry] classifiers = [ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries", ] packages = [ { include = "bleak_retry_connector", from = "src" }, ] [tool.poetry.dependencies] python = ">=3.10" bleak = {version = ">=0.21.0", python = ">=3.10,<3.14"} async-timeout = {version = ">=3.0.0", python = "<3.11"} dbus-fast = {version = ">=1.14.0", markers = "platform_system == \"Linux\""} bluetooth-adapters = {version = ">=0.15.2", markers = "platform_system == \"Linux\"", python = ">=3.10,<3.14"} [tool.poetry.group.dev.dependencies] dbus-fast = ">=1.4.0" pytest = "^8.3" pytest-cov = "^6.0" pytest-asyncio = "^0.25.3" [tool.semantic_release] branch = "main" version_toml = ["pyproject.toml:project.version"] version_variables = ["src/bleak_retry_connector/__init__.py:__version__"] build_command = "pip install poetry && poetry build" [tool.pytest.ini_options] addopts = "-v -Wdefault --cov=bleak_retry_connector --cov-report=term-missing:skip-covered" pythonpath = ["src"] [tool.coverage.run] branch = true [tool.coverage.report] exclude_lines = [ "pragma: no cover", "@overload", "if TYPE_CHECKING", "raise NotImplementedError", ] [tool.isort] profile = "black" known_first_party = ["bleak_retry_connector", "tests"] [tool.mypy] check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true mypy_path = "src/" no_implicit_optional = true show_error_codes = true warn_unreachable = true warn_unused_ignores = true exclude = [ 'docs/.*', 'setup.py', ] [[tool.mypy.overrides]] module = "tests.*" allow_untyped_defs = true [[tool.mypy.overrides]] module = "docs.*" ignore_errors = true [build-system] requires = ["poetry-core>=2.0.0"] build-backend = "poetry.core.masonry.api" bleak-retry-connector-3.10.0/renovate.json000066400000000000000000000001011477303675200205300ustar00rootroot00000000000000{ "extends": ["github>browniebroke/renovate-configs:python"] } bleak-retry-connector-3.10.0/setup.py000066400000000000000000000003731477303675200175370ustar00rootroot00000000000000#!/usr/bin/env python # This is a shim to allow GitHub to detect the package, build is done with poetry # Taken from https://github.com/Textualize/rich import setuptools if __name__ == "__main__": setuptools.setup(name="bleak-retry-connector") bleak-retry-connector-3.10.0/src/000077500000000000000000000000001477303675200166115ustar00rootroot00000000000000bleak-retry-connector-3.10.0/src/bleak_retry_connector/000077500000000000000000000000001477303675200231665ustar00rootroot00000000000000bleak-retry-connector-3.10.0/src/bleak_retry_connector/__init__.py000066400000000000000000000447011477303675200253050ustar00rootroot00000000000000from __future__ import annotations __version__ = "3.10.0" import asyncio import logging from collections.abc import Awaitable, Callable from typing import Any, ParamSpec, TypeVar from bleak import BleakClient, BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakDBusError, BleakDeviceNotFoundError, BleakError from .bluez import ( # noqa: F401 AllocationChange, AllocationChangeEvent, Allocations, BleakSlotManager, _get_properties, clear_cache, device_source, get_connected_devices, get_device, get_device_by_adapter, wait_for_device_to_reappear, wait_for_disconnect, ) from .const import IS_LINUX, NO_RSSI_VALUE, RSSI_SWITCH_THRESHOLD from .util import asyncio_timeout DISCONNECT_TIMEOUT = 5 DEFAULT_ATTEMPTS = 2 if IS_LINUX: from bluetooth_adapters import load_history_from_managed_objects from .dbus import disconnect_devices else: load_history_from_managed_objects = None disconnect_devices = None # type: ignore[assignment] # Make sure bleak and dbus-fast have time # to run their cleanup callbacks or the # retry call will just fail in the same way. BLEAK_TRANSIENT_BACKOFF_TIME = 0.25 BLEAK_TRANSIENT_MEDIUM_BACKOFF_TIME = 0.50 BLEAK_TRANSIENT_LONG_BACKOFF_TIME = 1.0 BLEAK_DBUS_BACKOFF_TIME = 0.25 BLEAK_OUT_OF_SLOTS_BACKOFF_TIME = 4.00 BLEAK_BACKOFF_TIME = 0.1 # Expected disconnect or ran out of slots # after checking, don't backoff since we # want to retry immediately. BLEAK_DISCONNECTED_BACKOFF_TIME = 0.0 __all__ = [ "BleakSlotManager", # Currently only possible for BlueZ, for MacOS we have no of knowing "ble_device_description", "establish_connection", "close_stale_connections", "close_stale_connections_by_address", "clear_cache", "get_device", "get_device_by_adapter", "device_source", "restore_discoveries", "retry_bluetooth_connection_error", "BleakClientWithServiceCache", "BleakAbortedError", "BleakNotFoundError", "BLEAK_RETRY_EXCEPTIONS", "RSSI_SWITCH_THRESHOLD", "NO_RSSI_VALUE", ] BLEAK_EXCEPTIONS = (AttributeError, BleakError) BLEAK_RETRY_EXCEPTIONS = ( *BLEAK_EXCEPTIONS, EOFError, BrokenPipeError, asyncio.TimeoutError, ) _LOGGER = logging.getLogger(__name__) MAX_TRANSIENT_ERRORS = 9 # Shorter time outs and more attempts # seems to be better for dbus, and corebluetooth # is happy either way. Ideally we want everything # to finish in < 60s or declare we cannot connect MAX_CONNECT_ATTEMPTS = 4 BLEAK_TIMEOUT = 20.0 # Bleak may not always timeout # since the dbus connection can stall # so we have an additional timeout to # be sure we do not block forever # This is likely fixed in https://github.com/hbldh/bleak/pull/1092 # # This also accounts for the time it # takes for the esp32s to disconnect # BLEAK_SAFETY_TIMEOUT = 60.0 TRANSIENT_ERRORS_LONG_BACKOFF = { "ESP_GATT_ERROR", } TRANSIENT_ERRORS_MEDIUM_BACKOFF = { "ESP_GATT_CONN_TIMEOUT", "ESP_GATT_CONN_FAIL_ESTABLISH", } DEVICE_MISSING_ERRORS = {"org.freedesktop.DBus.Error.UnknownObject"} OUT_OF_SLOTS_ERRORS = {"available connection", "connection slot"} TRANSIENT_ERRORS = { "le-connection-abort-by-local", "br-connection-canceled", "ESP_GATT_CONN_FAIL_ESTABLISH", "ESP_GATT_CONN_TERMINATE_PEER_USER", "ESP_GATT_CONN_TERMINATE_LOCAL_HOST", "ESP_GATT_CONN_CONN_CANCEL", } | OUT_OF_SLOTS_ERRORS # Currently the same as transient error ABORT_ERRORS = ( TRANSIENT_ERRORS | TRANSIENT_ERRORS_MEDIUM_BACKOFF | TRANSIENT_ERRORS_LONG_BACKOFF ) ABORT_ADVICE = ( "Interference/range; " "External Bluetooth adapter w/extension may help; " "Extension cables reduce USB 3 port interference" ) DEVICE_MISSING_ADVICE = ( "The device disappeared; " "Try restarting the scanner or moving the device closer" ) OUT_OF_SLOTS_ADVICE = ( "The proxy/adapter is out of connection slots or the device is no longer reachable; " "Add additional proxies (https://esphome.github.io/bluetooth-proxies/) near this device" ) NORMAL_DISCONNECT = "Disconnected" class BleakNotFoundError(BleakError): """The device was not found.""" class BleakConnectionError(BleakError): """The device was not found.""" class BleakAbortedError(BleakError): """The connection was aborted.""" class BleakOutOfConnectionSlotsError(BleakError): """The proxy/adapter is out of connection slots.""" class BleakClientWithServiceCache(BleakClient): """A BleakClient that implements service caching.""" def set_cached_services(self, services: BleakGATTServiceCollection | None) -> None: """Set the cached services. No longer used since bleak 0.17+ has service caching built-in. This was only kept for backwards compatibility. """ async def clear_cache(self) -> bool: """Clear the cached services.""" if hasattr(super(), "clear_cache"): return await super().clear_cache() _LOGGER.warning("clear_cache not implemented in bleak version") return False def ble_device_has_changed(original: BLEDevice, new: BLEDevice) -> bool: """Check if the device has changed.""" return bool( original.address != new.address or ( isinstance(original.details, dict) and isinstance(new.details, dict) and "path" in original.details and "path" in new.details and original.details["path"] != new.details["path"] ) ) def ble_device_description(device: BLEDevice) -> str: """Get the device description.""" details = device.details address = device.address name = device.name if name != address: base_name = f"{address} - {name}" else: base_name = address if isinstance(details, dict): if path := details.get("path"): # /org/bluez/hci2 return f"{base_name} -> {path[0:15]}" if source := details.get("source"): return f"{base_name} -> {source}" return base_name def calculate_backoff_time(exc: Exception) -> float: """Calculate the backoff time based on the exception.""" if isinstance( exc, (BleakDBusError, EOFError, asyncio.TimeoutError, BrokenPipeError) ): return BLEAK_DBUS_BACKOFF_TIME # If the adapter runs out of slots can get a BleakDeviceNotFoundError # since the device is no longer visible on the adapter. Almost none of # the adapters document how many connection slots they have so we cannot # know if we are out of slots or not. We can only guess based on the # error message and backoff. if isinstance(exc, (BleakDeviceNotFoundError, BleakNotFoundError)): return BLEAK_OUT_OF_SLOTS_BACKOFF_TIME if isinstance(exc, BleakError): bleak_error = str(exc) if any(error in bleak_error for error in OUT_OF_SLOTS_ERRORS): return BLEAK_OUT_OF_SLOTS_BACKOFF_TIME if any(error in bleak_error for error in TRANSIENT_ERRORS_MEDIUM_BACKOFF): return BLEAK_TRANSIENT_MEDIUM_BACKOFF_TIME if any(error in bleak_error for error in TRANSIENT_ERRORS_LONG_BACKOFF): return BLEAK_TRANSIENT_LONG_BACKOFF_TIME if any(error in bleak_error for error in TRANSIENT_ERRORS): return BLEAK_TRANSIENT_BACKOFF_TIME if NORMAL_DISCONNECT in bleak_error: return BLEAK_DISCONNECTED_BACKOFF_TIME return BLEAK_BACKOFF_TIME async def _disconnect_devices(devices: list[BLEDevice]) -> None: """Disconnect the devices.""" if IS_LINUX: await disconnect_devices(devices) async def close_stale_connections_by_address( address: str, only_other_adapters: bool = False ) -> None: """Close stale connections by address.""" if not IS_LINUX or not (device := await get_device(address)): return await close_stale_connections(device, only_other_adapters) async def close_stale_connections( device: BLEDevice, only_other_adapters: bool = False ) -> None: """Close stale connections.""" if not IS_LINUX or not (devices := await get_connected_devices(device)): return to_disconnect: list[BLEDevice] = [] for connected_device in devices: if only_other_adapters and not ble_device_has_changed(connected_device, device): _LOGGER.debug( "%s - %s: unexpectedly connected, not disconnecting since only_other_adapters is set", connected_device.name, connected_device.address, ) else: _LOGGER.debug( "%s - %s: unexpectedly connected, disconnecting", connected_device.name, connected_device.address, ) to_disconnect.append(connected_device) if not to_disconnect: return await _disconnect_devices(to_disconnect) AnyBleakClient = TypeVar("AnyBleakClient", bound=BleakClient) async def establish_connection( client_class: type[AnyBleakClient], device: BLEDevice, name: str, disconnected_callback: Callable[[AnyBleakClient], None] | None = None, max_attempts: int = MAX_CONNECT_ATTEMPTS, cached_services: BleakGATTServiceCollection | None = None, ble_device_callback: Callable[[], BLEDevice] | None = None, use_services_cache: bool = True, **kwargs: Any, ) -> AnyBleakClient: """Establish a connection to the device.""" timeouts = 0 connect_errors = 0 transient_errors = 0 attempt = 0 def _raise_if_needed(name: str, description: str, exc: Exception) -> None: """Raise if we reach the max attempts.""" if ( timeouts + connect_errors < max_attempts and transient_errors < MAX_TRANSIENT_ERRORS ): return msg = ( f"{name} - {description}: Failed to connect after " f"{attempt} attempt(s): {str(exc) or type(exc).__name__}" ) # Sure would be nice if bleak gave us typed exceptions if isinstance(exc, asyncio.TimeoutError): raise BleakNotFoundError(msg) from exc if isinstance(exc, BleakDeviceNotFoundError) or "not found" in str(exc): raise BleakNotFoundError(f"{msg}: {DEVICE_MISSING_ADVICE}") from exc if isinstance(exc, BleakError): if any(error in str(exc) for error in OUT_OF_SLOTS_ERRORS): raise BleakOutOfConnectionSlotsError( f"{msg}: {OUT_OF_SLOTS_ADVICE}" ) from exc if any(error in str(exc) for error in ABORT_ERRORS): raise BleakAbortedError(f"{msg}: {ABORT_ADVICE}") from exc if any(error in str(exc) for error in DEVICE_MISSING_ERRORS): raise BleakNotFoundError(f"{msg}: {DEVICE_MISSING_ADVICE}") from exc raise BleakConnectionError(msg) from exc debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) rssi: int | None = None if IS_LINUX and (devices := await get_connected_devices(device)): # Bleak 0.17 will handle already connected devices for us so # if we are already connected we swap the device to the connected # device. device = devices[0] client = client_class(device, disconnected_callback=disconnected_callback, **kwargs) while True: attempt += 1 if debug_enabled: _LOGGER.debug( "%s - %s: Connection attempt: %s", name, device.address, attempt, ) try: async with asyncio_timeout(BLEAK_SAFETY_TIMEOUT): await client.connect( timeout=BLEAK_TIMEOUT, dangerous_use_bleak_cache=use_services_cache or bool(cached_services), ) if debug_enabled: _LOGGER.debug( "%s - %s: Connected after %s attempts", name, device.address, attempt, ) except asyncio.TimeoutError as exc: timeouts += 1 if debug_enabled: _LOGGER.debug( "%s - %s: Timed out trying to connect (attempt: %s, last rssi: %s)", name, device.address, attempt, rssi, ) backoff_time = calculate_backoff_time(exc) await wait_for_disconnect(device, backoff_time) _raise_if_needed(name, device.address, exc) except KeyError as exc: # Likely: KeyError: 'org.bluez.GattService1' from bleak # ideally we would get a better error from bleak, but this is # better than nothing. # self._properties[service_path][defs.GATT_SERVICE_INTERFACE] transient_errors += 1 if debug_enabled: _LOGGER.debug( "%s - %s: Failed to connect due to services changes: %s (attempt: %s, last rssi: %s)", name, device.address, str(exc), attempt, rssi, ) if isinstance(client, BleakClientWithServiceCache): await client.clear_cache() await client.disconnect() backoff_time = calculate_backoff_time(exc) await wait_for_disconnect(device, backoff_time) _raise_if_needed(name, device.address, exc) except BrokenPipeError as exc: # BrokenPipeError is raised by dbus-next when the device disconnects # # bleak.exc.BleakDBusError: [org.bluez.Error] le-connection-abort-by-local # During handling of the above exception, another exception occurred: # Traceback (most recent call last): # File "bleak/backends/bluezdbus/client.py", line 177, in connect # reply = await self._bus.call( # File "dbus_next/aio/message_bus.py", line 63, in write_callback # self.offset += self.sock.send(self.buf[self.offset:]) # BrokenPipeError: [Errno 32] Broken pipe transient_errors += 1 if debug_enabled: _LOGGER.debug( "%s - %s: Failed to connect: %s (attempt: %s, last rssi: %s)", name, device.address, str(exc), attempt, rssi, ) _raise_if_needed(name, device.address, exc) except EOFError as exc: transient_errors += 1 backoff_time = calculate_backoff_time(exc) if debug_enabled: _LOGGER.debug( "%s - %s: Failed to connect: %s, backing off: %s (attempt: %s, last rssi: %s)", name, device.address, str(exc), backoff_time, attempt, rssi, ) await wait_for_disconnect(device, backoff_time) _raise_if_needed(name, device.address, exc) except BLEAK_EXCEPTIONS as exc: bleak_error = str(exc) # BleakDeviceNotFoundError can mean that the adapter has run out of # connection slots. device_missing = isinstance( exc, (BleakNotFoundError, BleakDeviceNotFoundError) ) if device_missing or any( error in bleak_error for error in TRANSIENT_ERRORS ): transient_errors += 1 else: connect_errors += 1 backoff_time = calculate_backoff_time(exc) if debug_enabled: _LOGGER.debug( "%s - %s: Failed to connect: %s, device_missing: %s, backing off: %s (attempt: %s, last rssi: %s)", name, device.address, bleak_error, device_missing, backoff_time, attempt, rssi, ) await wait_for_disconnect(device, backoff_time) _raise_if_needed(name, device.address, exc) else: return client # Ensure the disconnect callback # has a chance to run before we try to reconnect await asyncio.sleep(0) raise RuntimeError("This should never happen") P = ParamSpec("P") T = TypeVar("T") def retry_bluetooth_connection_error( attempts: int = DEFAULT_ATTEMPTS, ) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]: """Define a wrapper to retry on bluetooth connection error.""" def _decorator_retry_bluetooth_connection_error( func: Callable[P, Awaitable[T]], ) -> Callable[P, Awaitable[T]]: """Define a wrapper to retry on bleak error. The accessory is allowed to disconnect us any time so we need to retry the operation. """ async def _async_wrap_bluetooth_connection_error_retry( # type: ignore[return] *args: P.args, **kwargs: P.kwargs ) -> T: for attempt in range(attempts): try: return await func(*args, **kwargs) except BLEAK_EXCEPTIONS as ex: backoff_time = calculate_backoff_time(ex) if attempt == attempts - 1: raise _LOGGER.debug( "Bleak error calling %s, backing off: %s, retrying...", func, backoff_time, exc_info=True, ) await asyncio.sleep(backoff_time) return _async_wrap_bluetooth_connection_error_retry return _decorator_retry_bluetooth_connection_error async def restore_discoveries(scanner: BleakScanner, adapter: str) -> None: """Restore discoveries from the bus.""" if not IS_LINUX: # This is only supported on Linux return if not (properties := await _get_properties()): _LOGGER.debug("Failed to restore discoveries for %s", adapter) return backend = scanner._backend before = len(backend.seen_devices) backend.seen_devices.update( { address: (history.device, history.advertisement_data) for address, history in load_history_from_managed_objects( properties, adapter ).items() } ) _LOGGER.debug( "Restored %s discoveries for %s", len(backend.seen_devices) - before, adapter ) bleak-retry-connector-3.10.0/src/bleak_retry_connector/bleak_manager.py000066400000000000000000000043641477303675200263170ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextlib import logging from .const import DBUS_CONNECT_TIMEOUT, IS_LINUX from .util import asyncio_timeout _LOGGER = logging.getLogger(__name__) _global_instances: dict[asyncio.AbstractEventLoop, BlueZManager] | None = None if IS_LINUX: with contextlib.suppress(ImportError): # pragma: no cover from bleak.backends.bluezdbus.manager import ( # pragma: no cover BlueZManager, get_global_bluez_manager, ) with contextlib.suppress(ImportError): # pragma: no cover from bleak.backends.bluezdbus.manager import ( # type: ignore[no-redef] # pragma: no cover _global_instances, ) async def get_global_bluez_manager_with_timeout() -> BlueZManager | None: """Get the properties.""" if not IS_LINUX: return None loop = asyncio.get_running_loop() if _global_instances and (manager := _global_instances.get(loop)): return manager if ( getattr(get_global_bluez_manager_with_timeout, "_has_dbus_socket", None) is False ): # We are not running on a system with DBus do don't # keep trying to call get_global_bluez_manager as it # waits for a bit trying to connect to DBus. return None try: async with asyncio_timeout(DBUS_CONNECT_TIMEOUT): return await get_global_bluez_manager() except FileNotFoundError as ex: setattr(get_global_bluez_manager_with_timeout, "_has_dbus_socket", False) _LOGGER.debug( "Dbus socket at %s not found, will not try again until next restart: %s", ex.filename, ex, ) except asyncio.TimeoutError: setattr(get_global_bluez_manager_with_timeout, "_has_dbus_socket", False) _LOGGER.debug( "Timed out trying to connect to DBus; will not try again until next restart" ) except Exception as ex: # pylint: disable=broad-except _LOGGER.debug( "get_global_bluez_manager_with_timeout failed: %s", ex, exc_info=True ) return None def _reset_dbus_socket_cache() -> None: """Reset the dbus socket cache.""" setattr(get_global_bluez_manager_with_timeout, "_has_dbus_socket", None) bleak-retry-connector-3.10.0/src/bleak_retry_connector/bluez.py000066400000000000000000000460061477303675200246670ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextlib import logging import time from collections.abc import Callable, Generator from dataclasses import dataclass from enum import Enum from functools import partial from typing import Any from bleak.backends.device import BLEDevice from bleak.exc import BleakError from .bleak_manager import get_global_bluez_manager_with_timeout from .const import ( DISCONNECT_TIMEOUT, IS_LINUX, NO_RSSI_VALUE, REAPPEAR_WAIT_INTERVAL, RSSI_SWITCH_THRESHOLD, ) from .util import asyncio_timeout if IS_LINUX: from dbus_fast.message import Message _LOGGER = logging.getLogger(__name__) if IS_LINUX: with contextlib.suppress(ImportError): # pragma: no cover from bleak.backends.bluezdbus import defs # pragma: no cover from bleak.backends.bluezdbus.manager import ( # pragma: no cover BlueZManager, DeviceWatcher, ) class AllocationChange(Enum): """Allocation change.""" ALLOCATED = 1 RELEASED = 2 @dataclass(slots=True) class AllocationChangeEvent: change: AllocationChange path: str | None # D-Bus object path of the device adapter: str # Adapter/Controller (hciX) address: str # Address of the remote BLE device @dataclass(slots=True) class Allocations: adapter: str # Adapter/Controller (hciX) slots: int # Number of slots free: int # Number of free slots allocated: list[str] # Addresses of connected devices def device_source(device: BLEDevice) -> str | None: """Return the device source.""" return _device_details_value_or_none(device, "source") def _device_details_value_or_none(device: BLEDevice, key: str) -> Any | None: """Return a value from device details or None.""" details = device.details if not isinstance(details, dict) or key not in details: return None key_value: str = device.details[key] return key_value def adapter_from_path(path: str) -> str: """Get the adapter from a ble device path.""" return path.split("/")[3] def address_from_path(path: str) -> str: """Get the address from a ble device path.""" return path.split("/")[-1].removeprefix("dev_").replace("_", ":").upper() def path_from_ble_device(device: BLEDevice) -> str | None: """Get the adapter from a ble device.""" return _device_details_value_or_none(device, "path") def _on_characteristic_value_changed(*args: Any, **kwargs: Any) -> None: """Dummy callback for registering characteristic value changed.""" class BleakSlotManager: """A class to manage the connection slots.""" def __init__(self) -> None: """Initialize the class.""" self._adapter_slots: dict[str, int] = {} self._allocations_by_adapter: dict[str, dict[str, DeviceWatcher]] = {} self._manager: BlueZManager | None = None self._callbacks: set[Callable[[AllocationChangeEvent], None]] = set() async def async_setup(self) -> None: """Set up the class.""" self._manager = await get_global_bluez_manager_with_timeout() def diagnostics(self) -> dict[str, Any]: """Return diagnostics.""" return { "manager": self._manager is not None, "adapter_slots": self._adapter_slots, "allocations_by_adapter": { adapter: self._get_allocations(adapter) for adapter in self._adapter_slots }, } def get_allocations(self, adapter: str) -> Allocations: """Get the allocations.""" slots = self._adapter_slots.get(adapter, 0) allocated = [ address_from_path(path) for path in self._allocations_by_adapter[adapter] ] free = slots - len(allocated) return Allocations(adapter, slots, free, allocated) def _get_allocations(self, adapter: str) -> list[str]: """Get connected path allocations.""" if self._manager is None: return [] return list(self._allocations_by_adapter[adapter]) def remove_adapter(self, adapter: str) -> None: """Remove an adapter.""" del self._adapter_slots[adapter] watchers = self._allocations_by_adapter[adapter] if self._manager is None: return for watcher in watchers.values(): self._manager.remove_device_watcher(watcher) del self._allocations_by_adapter[adapter] def register_allocation_callback( self, callback: Callable[[AllocationChangeEvent], None] ) -> Callable[[], None]: """Register a callback for when allocations change.""" self._callbacks.add(callback) return partial(self.unregister_allocation_callback, callback) def unregister_allocation_callback( self, callback: Callable[[AllocationChangeEvent], None] ) -> None: """Unregister a callback.""" self._callbacks.discard(callback) def register_adapter(self, adapter: str, slots: int) -> None: """Register an adapter.""" self._allocations_by_adapter[adapter] = {} self._adapter_slots[adapter] = slots if self._manager is None: return for path, device in self._manager._properties.items(): if ( defs.DEVICE_INTERFACE in device and device[defs.DEVICE_INTERFACE].get("Connected") and adapter_from_path(path) == adapter ): self._allocate_and_watch_slot(path) def _allocate_and_watch_slot(self, path: str) -> None: """Setup a device watcher.""" assert self._manager is not None # nosec adapter = adapter_from_path(path) allocations = self._allocations_by_adapter[adapter] def _on_device_connected_changed(connected: bool) -> None: if not connected: self._release_slot(path) allocations[path] = self._manager.add_device_watcher( path, on_connected_changed=_on_device_connected_changed, on_characteristic_value_changed=_on_characteristic_value_changed, ) self._call_callbacks(AllocationChange.ALLOCATED, path) def release_slot(self, device: BLEDevice) -> None: """Release a slot.""" if ( self._manager is None or not (path := path_from_ble_device(device)) or self._manager.is_connected(path) ): return self._release_slot(path) def _release_slot(self, path: str) -> None: """Unconditional release of the slot.""" assert self._manager is not None # nosec adapter = adapter_from_path(path) allocations = self._allocations_by_adapter[adapter] if watcher := allocations.pop(path, None): self._manager.remove_device_watcher(watcher) self._call_callbacks(AllocationChange.RELEASED, path) def _call_callbacks(self, change: AllocationChange, path: str) -> None: """Call the callbacks.""" for callback_ in self._callbacks: try: callback_( AllocationChangeEvent( change, path, adapter_from_path(path), address_from_path(path) ) ) except Exception: # pylint _LOGGER.exception("Error in callback") def allocate_slot(self, device: BLEDevice) -> bool: """Allocate a slot.""" if ( self._manager is None or not (path := path_from_ble_device(device)) or not (adapter := adapter_from_path(path)) or adapter not in self._allocations_by_adapter ): return True allocations = self._allocations_by_adapter[adapter] if path in allocations: # Already connected return True if len(allocations) >= self._adapter_slots[adapter]: _LOGGER.debug( "No slots available for %s (used by: %s)", path, self._get_allocations(adapter), ) return False self._allocate_and_watch_slot(path) return True async def _get_properties() -> dict[str, dict[str, dict[str, Any]]] | None: """Get the properties.""" if bluez_manager := await get_global_bluez_manager_with_timeout(): return bluez_manager._properties # pylint: disable=protected-access return None async def clear_cache(address: str) -> bool: """Clear the cache for a device.""" if not IS_LINUX or not await get_device(address): return False caches_cleared: list[str] = [] with contextlib.suppress(Exception): if not (manager := await get_global_bluez_manager_with_timeout()): _LOGGER.warning("Failed to clear cache for %s because no manager", address) return False services_cache = manager._services_cache bluez_path = address_to_bluez_path(address) for path in _get_possible_paths(bluez_path): if services_cache.pop(path, None): caches_cleared.append(path) _LOGGER.debug("Cleared cache for %s: %s", address, caches_cleared) async with asyncio_timeout(DISCONNECT_TIMEOUT): for device_path in caches_cleared: # Send since we are going to ignore errors # in case the device is already gone await manager._bus.send( Message( destination=defs.BLUEZ_SERVICE, path=adapter_path_from_device_path(device_path), interface=defs.ADAPTER_INTERFACE, member="RemoveDevice", signature="o", body=[device_path], ) ) return bool(caches_cleared) async def stop_discovery(adapter_name: str) -> None: """Stop discovery on an adapter. :param adapter_name: The adapter name (hciX). """ if manager := await get_global_bluez_manager_with_timeout(): adapter_path = f"/org/bluez/{adapter_name}" await manager._bus.send( Message( destination=defs.BLUEZ_SERVICE, path=adapter_path, interface=defs.ADAPTER_INTERFACE, member="StopDiscovery", ) ) else: _LOGGER.error( "Failed to stop discovery for %s because no manager", adapter_name ) def adapter_path_from_device_path(device_path: str) -> str: """ Scrape the adapter path from a D-Bus device path. Args: device_path: The D-Bus object path of the device. Returns: A D-Bus object path of the adapter. """ # /org/bluez/hci1/dev_FA_23_9D_AA_45_46 return device_path[:15] async def wait_for_device_to_reappear(device: BLEDevice, wait_timeout: float) -> bool: """Wait for a device to reappear on the bus.""" await asyncio.sleep(0) if ( not IS_LINUX or not isinstance(device.details, dict) or "path" not in device.details or not (properties := await _get_properties()) ): await asyncio.sleep(wait_timeout) return False debug = _LOGGER.isEnabledFor(logging.DEBUG) device_path = address_to_bluez_path(device.address) for i in range(int(wait_timeout / REAPPEAR_WAIT_INTERVAL)): for path in _get_possible_paths(device_path): if path in properties and properties[path].get(defs.DEVICE_INTERFACE): if debug: _LOGGER.debug( "%s - %s: Device re-appeared on bus after %s seconds as %s", device.name, device.address, i * REAPPEAR_WAIT_INTERVAL, path, ) return True if debug: _LOGGER.debug( "%s - %s: Waiting %s/%s for device to re-appear on bus", device.name, device.address, (i + 1) * REAPPEAR_WAIT_INTERVAL, wait_timeout, ) await asyncio.sleep(REAPPEAR_WAIT_INTERVAL) if debug: _LOGGER.debug( "%s - %s: Device did not re-appear on bus after %s seconds", device.name, device.address, wait_timeout, ) return False async def wait_for_disconnect(device: BLEDevice, min_wait_time: float) -> None: """Wait for the device to disconnect. After a connection failure, the device may not have had time to disconnect so we wait for it to do so. If we do not wait, we may end up connecting to the same device again before it has had time to disconnect. """ if ( not IS_LINUX or not isinstance(device.details, dict) or "path" not in device.details ): await asyncio.sleep(min_wait_time) return device_path = device.details["path"] start = time.monotonic() if min_wait_time else 0 try: if not (manager := await get_global_bluez_manager_with_timeout()): _LOGGER.debug( "%s - %s: Failed to wait for disconnect because no manager", device.name, device.address, ) return async with asyncio_timeout(DISCONNECT_TIMEOUT): await manager._wait_condition(device_path, "Connected", False) end = time.monotonic() if min_wait_time else 0 waited = end - start _LOGGER.debug( "%s - %s: Waited %s seconds to disconnect", device.name, device.address, waited, ) if min_wait_time and waited < min_wait_time: await asyncio.sleep(min_wait_time - waited) except (BleakError, KeyError) as ex: # Device was removed from bus # # In testing it was found that most of the CSR adapters # only support 5 slots and the broadcom only support 7 slots. # # When they run out of slots the device they are trying to # connect to disappears from the bus so we must backoff _LOGGER.debug( "%s - %s: Device was removed from bus at %s, waiting %s for it to re-appear: (%s) %s", device.name, device.address, device_path, min_wait_time, type(ex), ex, ) await wait_for_device_to_reappear(device, min_wait_time) except Exception: # pylint: disable=broad-except _LOGGER.debug( "%s - %s: Failed waiting for disconnect at %s", device.name, device.address, device_path, exc_info=True, ) async def get_device_by_adapter(address: str, adapter: str) -> BLEDevice | None: """Get the device by adapter and address.""" if not IS_LINUX: return None if not (properties := await _get_properties()): return None device_path = address_to_bluez_path(address, adapter) if device_path in properties and ( device_props := properties[device_path].get(defs.DEVICE_INTERFACE) ): return ble_device_from_properties(device_path, device_props) return None async def get_bluez_device( name: str, path: str, rssi: int | None = None, _log_disappearance: bool = True ) -> BLEDevice | None: """Get a BLEDevice object for a BlueZ DBus path.""" best_path = device_path = path rssi_to_beat: int = rssi or NO_RSSI_VALUE if not (properties := await _get_properties()): return None if ( device_path not in properties or defs.DEVICE_INTERFACE not in properties[device_path] ): # device has disappeared so take # anything over the current path if _log_disappearance: _LOGGER.debug("%s - %s: Device has disappeared", name, device_path) rssi_to_beat = NO_RSSI_VALUE for path in _get_possible_paths(device_path): if path not in properties or not ( device_props := properties[path].get(defs.DEVICE_INTERFACE) ): continue if device_props.get("Connected"): # device is connected so take it _LOGGER.debug("%s - %s: Device is already connected", name, path) if path == device_path: # device is connected to the path we were given # so we can just return None so it will be used return None return ble_device_from_properties(path, device_props) if path == device_path: # Device is not connected and is the original path # so no need to check it since returning None will # cause the device to be used anyways. continue alternate_device_rssi: int = device_props.get("RSSI") or NO_RSSI_VALUE if ( rssi_to_beat != NO_RSSI_VALUE and alternate_device_rssi - RSSI_SWITCH_THRESHOLD < rssi_to_beat ): continue best_path = path _LOGGER.debug( "%s - %s: Found path %s with better RSSI %s > %s", name, device_path, path, alternate_device_rssi, rssi_to_beat, ) rssi_to_beat = alternate_device_rssi if best_path == device_path: return None return ble_device_from_properties( best_path, properties[best_path][defs.DEVICE_INTERFACE] ) async def get_connected_devices(device: BLEDevice) -> list[BLEDevice]: """Check if the device is connected.""" connected: list[BLEDevice] = [] if not isinstance(device.details, dict) or "path" not in device.details: return connected if not (properties := await _get_properties()): return connected device_path = device.details["path"] for path in _get_possible_paths(device_path): if path not in properties or defs.DEVICE_INTERFACE not in properties[path]: continue props = properties[path][defs.DEVICE_INTERFACE] if props.get("Connected"): connected.append(ble_device_from_properties(path, props)) return connected async def get_device(address: str) -> BLEDevice | None: """Get the device.""" if not IS_LINUX: return None return await get_bluez_device( address, address_to_bluez_path(address), _log_disappearance=False ) def address_to_bluez_path(address: str, adapter: str | None = None) -> str: """Convert an address to a BlueZ path.""" return f"/org/bluez/{adapter or 'hciX'}/dev_{address.upper().replace(':', '_')}" def _get_possible_paths(path: str) -> Generator[str]: """Get the possible paths.""" # The path is deterministic so we splice up the string # /org/bluez/hci2/dev_FA_23_9D_AA_45_46 for i in range(0, 9): yield f"{path[0:14]}{i}{path[15:]}" def ble_device_from_properties(path: str, props: dict[str, Any]) -> BLEDevice: """Get a BLEDevice from a dict of properties.""" return BLEDevice( props["Address"], props["Alias"], {"path": path, "props": props}, props.get("RSSI") or NO_RSSI_VALUE, uuids=props.get("UUIDs", []), manufacturer_data={ k: bytes(v) for k, v in props.get("ManufacturerData", {}).items() }, ) bleak-retry-connector-3.10.0/src/bleak_retry_connector/const.py000066400000000000000000000003331477303675200246650ustar00rootroot00000000000000from __future__ import annotations import platform IS_LINUX = platform.system() == "Linux" NO_RSSI_VALUE = -127 RSSI_SWITCH_THRESHOLD = 5 DISCONNECT_TIMEOUT = 5 REAPPEAR_WAIT_INTERVAL = 0.5 DBUS_CONNECT_TIMEOUT = 8.5 bleak-retry-connector-3.10.0/src/bleak_retry_connector/dbus.py000066400000000000000000000025311477303675200244760ustar00rootroot00000000000000from __future__ import annotations import contextlib from bleak.backends.bluezdbus import defs from bleak.backends.device import BLEDevice from dbus_fast.message import Message from .bleak_manager import get_global_bluez_manager_with_timeout from .const import DISCONNECT_TIMEOUT from .util import asyncio_timeout async def disconnect_devices(devices: list[BLEDevice]) -> None: """Disconnect a list of devices.""" valid_devices = [ device for device in devices if isinstance(device.details, dict) and "path" in device.details ] if not valid_devices: return if not (bluez_manager := await get_global_bluez_manager_with_timeout()): return bus = bluez_manager._bus for device in valid_devices: # https://bleak.readthedocs.io/en/latest/troubleshooting.html#id4 # Try to remove the device as well in the hope that it will # clear the disk cache of the device. with contextlib.suppress(Exception): async with asyncio_timeout(DISCONNECT_TIMEOUT): await bus.call( Message( destination=defs.BLUEZ_SERVICE, path=device.details["path"], interface=defs.DEVICE_INTERFACE, member="Disconnect", ) ) bleak-retry-connector-3.10.0/src/bleak_retry_connector/py.typed000066400000000000000000000000001477303675200246530ustar00rootroot00000000000000bleak-retry-connector-3.10.0/src/bleak_retry_connector/util.py000066400000000000000000000004301477303675200245120ustar00rootroot00000000000000from __future__ import annotations import sys if sys.version_info[:2] < (3, 11): from async_timeout import ( # noqa: F401 # pragma: no cover timeout as asyncio_timeout, ) else: from asyncio import timeout as asyncio_timeout # noqa: F401 # pragma: no cover bleak-retry-connector-3.10.0/tests/000077500000000000000000000000001477303675200171645ustar00rootroot00000000000000bleak-retry-connector-3.10.0/tests/__init__.py000066400000000000000000000000001477303675200212630ustar00rootroot00000000000000bleak-retry-connector-3.10.0/tests/conftest.py000066400000000000000000000015041477303675200213630ustar00rootroot00000000000000import logging from unittest.mock import patch import pytest import bleak_retry_connector @pytest.fixture(autouse=True) def configure_test_logging(caplog): caplog.set_level(logging.DEBUG) @pytest.fixture() def mock_linux(): with ( patch.object(bleak_retry_connector, "IS_LINUX", True), patch.object(bleak_retry_connector.bluez, "IS_LINUX", True), patch.object(bleak_retry_connector.bleak_manager, "IS_LINUX", True), patch("bleak.backends.scanner.platform.system", return_value="Linux"), ): yield @pytest.fixture() def mock_macos(): with ( patch.object(bleak_retry_connector, "IS_LINUX", False), patch.object(bleak_retry_connector.bluez, "IS_LINUX", False), patch.object(bleak_retry_connector.bleak_manager, "IS_LINUX", False), ): yield bleak-retry-connector-3.10.0/tests/test_bluez.py000066400000000000000000000503101477303675200217150ustar00rootroot00000000000000from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest from bleak.backends.bluezdbus import defs from bleak.backends.bluezdbus.manager import DeviceWatcher from bleak.backends.device import BLEDevice import bleak_retry_connector from bleak_retry_connector import ( AllocationChange, AllocationChangeEvent, Allocations, BleakSlotManager, device_source, ) from bleak_retry_connector.bluez import ( adapter_path_from_device_path, ble_device_from_properties, path_from_ble_device, stop_discovery, wait_for_device_to_reappear, ) pytestmark = pytest.mark.asyncio async def test_slot_manager(mock_linux): """Test the slot manager""" class FakeBluezManager: def __init__(self): self.watchers: set[DeviceWatcher] = set() self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Connected": True, "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Connected": True, "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } def add_device_watcher(self, path: str, **kwargs: Any) -> DeviceWatcher: """Add a watcher for device changes.""" watcher = DeviceWatcher(path, **kwargs) self.watchers.add(watcher) return watcher def remove_device_watcher(self, watcher: DeviceWatcher) -> None: """Remove a watcher for device changes.""" self.watchers.remove(watcher) def is_connected(self, path: str) -> bool: """Check if device is connected.""" return False bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs slot_manager = BleakSlotManager() await slot_manager.async_setup() slot_manager.register_adapter("hci0", 1) slot_manager.register_adapter("hci1", 2) slot_manager.register_adapter("hci2", 1) changes = [] def _failing_allocation_callback(event: AllocationChangeEvent) -> None: raise Exception("Test") def _allocation_callback(event: AllocationChangeEvent) -> None: change = event.change path = event.path adapter = event.adapter address = event.address changes.append((change, path, adapter, address)) cancel_fail = slot_manager.register_allocation_callback( _failing_allocation_callback ) cancel = slot_manager.register_allocation_callback(_allocation_callback) ble_device_hci2 = ble_device_from_properties( "/org/bluez/hci2/dev_FA_23_9D_AA_45_45", { "Address": "FA:23:9D:AA:45:45", "Alias": "FA:23:9D:AA:45:45", "RSSI": -30, }, ) ble_device_hci2_already_connected = ble_device_from_properties( "/org/bluez/hci2/dev_FA_23_9D_AA_45_46", { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, ) ble_device_hci0 = ble_device_from_properties( "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, ) ble_device_hci0_2 = ble_device_from_properties( "/org/bluez/hci0/dev_FA_23_9D_AA_45_47", { "Address": "FA:23:9D:AA:45:47", "Alias": "FA:23:9D:AA:45:47", "RSSI": -30, }, ) assert slot_manager.allocate_slot(ble_device_hci2) is False assert not changes # Make sure we can allocate an already connected device # since there is always a race condition between the # slot manager and the device connecting assert slot_manager.allocate_slot(ble_device_hci2_already_connected) is True assert not changes assert slot_manager.allocate_slot(ble_device_hci0) is True assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ) ] assert slot_manager._get_allocations("hci0") == [ "/org/bluez/hci0/dev_FA_23_9D_AA_45_46" ] assert slot_manager.get_allocations("hci0") == Allocations( "hci0", 1, 0, ["FA:23:9D:AA:45:46"], ) # Make sure we can allocate the same device again assert slot_manager.allocate_slot(ble_device_hci0) is True assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ] assert slot_manager._get_allocations("hci0") == [ "/org/bluez/hci0/dev_FA_23_9D_AA_45_46" ] assert slot_manager.get_allocations("hci0") == Allocations( "hci0", 1, 0, ["FA:23:9D:AA:45:46"], ) assert slot_manager.allocate_slot(ble_device_hci0_2) is False assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ] assert slot_manager._get_allocations("hci0") == [ "/org/bluez/hci0/dev_FA_23_9D_AA_45_46" ] assert slot_manager.get_allocations("hci0") == Allocations( "hci0", 1, 0, ["FA:23:9D:AA:45:46"], ) watcher: DeviceWatcher = slot_manager._allocations_by_adapter["hci0"][ "/org/bluez/hci0/dev_FA_23_9D_AA_45_46" ] watcher.on_connected_changed(True) assert slot_manager._get_allocations("hci0") == [ "/org/bluez/hci0/dev_FA_23_9D_AA_45_46" ] assert slot_manager.get_allocations("hci0") == Allocations( "hci0", 1, 0, ["FA:23:9D:AA:45:46"], ) assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ] watcher.on_connected_changed(False) assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ] assert slot_manager._get_allocations("hci0") == [] assert slot_manager.get_allocations("hci0") == Allocations( "hci0", 1, 1, [], ) assert slot_manager.allocate_slot(ble_device_hci0) is True assert slot_manager._get_allocations("hci0") == [ "/org/bluez/hci0/dev_FA_23_9D_AA_45_46" ] assert slot_manager.get_allocations("hci0") == Allocations( "hci0", 1, 0, ["FA:23:9D:AA:45:46"], ) assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ] assert slot_manager.diagnostics() == { "adapter_slots": {"hci0": 1, "hci1": 2, "hci2": 1}, "allocations_by_adapter": { "hci0": ["/org/bluez/hci0/dev_FA_23_9D_AA_45_46"], "hci1": ["/org/bluez/hci1/dev_FA_23_9D_AA_45_46"], "hci2": ["/org/bluez/hci2/dev_FA_23_9D_AA_45_46"], }, "manager": True, } slot_manager.release_slot(ble_device_hci0) assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ] assert slot_manager._get_allocations("hci0") == [] assert slot_manager.get_allocations("hci0") == Allocations( "hci0", 1, 1, [], ) assert slot_manager.allocate_slot(ble_device_hci0) is True assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ] assert slot_manager._get_allocations("hci0") == [ "/org/bluez/hci0/dev_FA_23_9D_AA_45_46" ] assert slot_manager.get_allocations("hci0") == Allocations( "hci0", 1, 0, ["FA:23:9D:AA:45:46"], ) slot_manager.remove_adapter("hci0") assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ] assert slot_manager.allocate_slot(ble_device_hci0) is True assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ] assert slot_manager.allocate_slot(ble_device_hci0_2) is True assert changes == [ ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.RELEASED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ( AllocationChange.ALLOCATED, "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "hci0", "FA:23:9D:AA:45:46", ), ] cancel_fail() cancel() async def test_slot_manager_mac_os(): """Test the slot manager""" bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=None ) bleak_retry_connector.bluez.defs = defs slot_manager = BleakSlotManager() await slot_manager.async_setup() slot_manager.register_adapter("hci0", 1) slot_manager.register_adapter("hci1", 2) slot_manager.register_adapter("hci2", 1) ble_device_hci0 = ble_device_from_properties( "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, ) ble_device_hci0_2 = ble_device_from_properties( "/org/bluez/hci0/dev_FA_23_9D_AA_45_47", { "Address": "FA:23:9D:AA:45:47", "Alias": "FA:23:9D:AA:45:47", "RSSI": -30, }, ) assert slot_manager.allocate_slot(ble_device_hci0) is True assert slot_manager._get_allocations("hci0") == [] assert slot_manager.allocate_slot(ble_device_hci0_2) is True assert slot_manager._get_allocations("hci0") == [] assert slot_manager.allocate_slot(ble_device_hci0) is True assert slot_manager._get_allocations("hci0") == [] slot_manager.release_slot(ble_device_hci0) assert slot_manager._get_allocations("hci0") == [] slot_manager.remove_adapter("hci0") def test_device_source(): ble_device_hci0_2 = BLEDevice( "FA:23:9D:AA:45:46", "FA:23:9D:AA:45:46", { "source": "aa:bb:cc:dd:ee:ff", "path": "/org/bluez/hci0/dev_FA_23_9D_AA_45_47", "props": {}, }, -127, uuids=[], manufacturer_data={}, ) assert device_source(ble_device_hci0_2) == "aa:bb:cc:dd:ee:ff" def test_path_from_ble_device(): ble_device_hci0_2 = BLEDevice( "FA:23:9D:AA:45:46", "FA:23:9D:AA:45:46", { "source": "aa:bb:cc:dd:ee:ff", "path": "/org/bluez/hci0/dev_FA_23_9D_AA_45_47", "props": {}, }, -127, uuids=[], manufacturer_data={}, ) assert ( path_from_ble_device(ble_device_hci0_2) == "/org/bluez/hci0/dev_FA_23_9D_AA_45_47" ) async def test_wait_for_device_to_reappear(mock_linux): class FakeBluezManager: def __init__(self): self.watchers: set[DeviceWatcher] = set() self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Connected": True, "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, } def add_device_watcher(self, path: str, **kwargs: Any) -> DeviceWatcher: """Add a watcher for device changes.""" watcher = DeviceWatcher(path, **kwargs) self.watchers.add(watcher) return watcher def remove_device_watcher(self, watcher: DeviceWatcher) -> None: """Remove a watcher for device changes.""" self.watchers.remove(watcher) def is_connected(self, path: str) -> bool: """Check if device is connected.""" return False bluez_manager = FakeBluezManager() bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=bluez_manager ) bleak_retry_connector.bluez.defs = defs ble_device_hci0 = BLEDevice( "FA:23:9D:AA:45:46", "FA:23:9D:AA:45:46", { "source": "aa:bb:cc:dd:ee:ff", "path": "/org/bluez/hci0/dev_FA_23_9D_AA_45_46", "props": {}, }, -127, uuids=[], manufacturer_data={}, ) assert await wait_for_device_to_reappear(ble_device_hci0, 1) is True del bluez_manager._properties["/org/bluez/hci0/dev_FA_23_9D_AA_45_46"] assert await wait_for_device_to_reappear(ble_device_hci0, 1) is True del bluez_manager._properties["/org/bluez/hci1/dev_FA_23_9D_AA_45_46"] with patch.object(bleak_retry_connector.bluez, "REAPPEAR_WAIT_INTERVAL", 0.025): assert await wait_for_device_to_reappear(ble_device_hci0, 0.1) is False async def test_adapter_path_from_device_path(mock_linux): assert ( adapter_path_from_device_path("/org/bluez/hci1/dev_FA_23_9D_AA_45_46") == "/org/bluez/hci1" ) async def test_stop_discovery(mock_linux): """Test stopping discovery""" class FakeBluezManager: def __init__(self) -> None: """Mock initializer.""" self._bus = MagicMock(send=AsyncMock()) manager = FakeBluezManager() bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=manager ) bleak_retry_connector.bluez.defs = defs bleak_retry_connector.bluez.Message = MagicMock() await stop_discovery("hci0") assert manager._bus.send.called async def test_stop_discovery_no_manager( mock_linux: None, caplog: pytest.LogCaptureFixture ) -> None: """Test stopping discovery no manager.""" bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=None ) bleak_retry_connector.bluez.defs = defs bleak_retry_connector.bluez.Message = MagicMock() await stop_discovery("hci0") assert "Failed to stop discovery" in caplog.text bleak-retry-connector-3.10.0/tests/test_init.py000066400000000000000000001773261477303675200215600ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from bleak import BleakClient, BleakError from bleak.backends.bluezdbus import defs from bleak.backends.bluezdbus.manager import DeviceWatcher from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakDBusError, BleakDeviceNotFoundError import bleak_retry_connector from bleak_retry_connector import ( BLEAK_BACKOFF_TIME, BLEAK_DBUS_BACKOFF_TIME, BLEAK_OUT_OF_SLOTS_BACKOFF_TIME, BLEAK_TRANSIENT_BACKOFF_TIME, BLEAK_TRANSIENT_LONG_BACKOFF_TIME, BLEAK_TRANSIENT_MEDIUM_BACKOFF_TIME, MAX_TRANSIENT_ERRORS, BleakAbortedError, BleakClientWithServiceCache, BleakConnectionError, BleakNotFoundError, BleakOutOfConnectionSlotsError, ble_device_description, ble_device_has_changed, calculate_backoff_time, clear_cache, close_stale_connections_by_address, establish_connection, get_connected_devices, get_device, get_device_by_adapter, restore_discoveries, retry_bluetooth_connection_error, ) from bleak_retry_connector.bleak_manager import _reset_dbus_socket_cache @pytest.mark.asyncio async def test_establish_connection_works_first_time(): class FakeBleakClient(BleakClient): async def connect(self, *args, **kwargs): pass async def disconnect(self, *args, **kwargs): pass client = await establish_connection( FakeBleakClient, MagicMock(), "test", disconnected_callback=MagicMock() ) assert isinstance(client, FakeBleakClient) @pytest.mark.asyncio async def test_establish_connection_with_cached_services(): class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._device_path = "/dev/test" async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" async def get_services(self, *args, **kwargs): return [] collection = BleakGATTServiceCollection() class FakeBluezManager: def __init__(self): self._properties = { "/dev/test/service/1": { "UUID": "service", "Primary": True, "Characteristics": [], defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs client = await establish_connection( FakeBleakClientWithServiceCache, MagicMock(), "test", disconnected_callback=MagicMock(), cached_services=collection, ) assert isinstance(client, FakeBleakClientWithServiceCache) await client.get_services() is collection @pytest.mark.asyncio async def test_establish_connection_with_cached_services_that_have_vanished(): class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._device_path = "/dev/test" async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" async def get_services(self, *args, **kwargs): return [] collection = BleakGATTServiceCollection() class FakeBluezManager: def __init__(self): self._properties = {} bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs client = await establish_connection( FakeBleakClientWithServiceCache, MagicMock(), "test", disconnected_callback=MagicMock(), cached_services=collection, ) assert isinstance(client, FakeBleakClientWithServiceCache) await client.get_services() is collection @pytest.mark.asyncio async def test_establish_connection_can_cache_services_always_patched(): class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._device_path = "/dev/test" async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" collection = BleakGATTServiceCollection() class FakeBluezManager: def __init__(self): self._properties = { "/dev/test/service/1": { "UUID": "service", "Primary": True, "Characteristics": [], defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs client = await establish_connection( FakeBleakClientWithServiceCache, MagicMock(), "test", disconnected_callback=MagicMock(), cached_services=collection, ) assert isinstance(client, FakeBleakClientWithServiceCache) await client.get_services() is collection @pytest.mark.asyncio async def test_establish_connection_can_cache_services_services_missing(): class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._device_path = "/dev/test" async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" collection = BleakGATTServiceCollection() class FakeBluezManager: def __init__(self): self._properties = { "/dev/test2/service/1": { "UUID": "service", "Primary": True, "Characteristics": [], }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs client = await establish_connection( FakeBleakClientWithServiceCache, MagicMock(), "test", disconnected_callback=MagicMock(), cached_services=collection, ) assert isinstance(client, FakeBleakClientWithServiceCache) await client.get_services() is collection @pytest.mark.asyncio async def test_establish_connection_can_cache_services_newer_bleak(): class FakeBleakClient(BleakClient): async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" collection = BleakGATTServiceCollection() client = await establish_connection( FakeBleakClientWithServiceCache, MagicMock(), "test", disconnected_callback=MagicMock(), cached_services=collection, ) assert isinstance(client, FakeBleakClientWithServiceCache) await client.get_services() is collection @pytest.mark.asyncio async def test_establish_connection_with_dangerous_use_cached_services(): class FakeBleakClient(BleakClient): async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" client = await establish_connection( FakeBleakClientWithServiceCache, MagicMock(), "test", disconnected_callback=MagicMock(), ) assert isinstance(client, FakeBleakClientWithServiceCache) @pytest.mark.asyncio async def test_establish_connection_without_dangerous_use_cached_services(): class FakeBleakClient(BleakClient): async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" client = await establish_connection( FakeBleakClientWithServiceCache, MagicMock(), "test", disconnected_callback=MagicMock(), ) assert isinstance(client, FakeBleakClientWithServiceCache) @pytest.mark.asyncio async def test_establish_connection_fails(): class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): raise BleakError("test") pass async def disconnect(self, *args, **kwargs): pass with ( patch("bleak_retry_connector.calculate_backoff_time", return_value=0), pytest.raises(BleakConnectionError), ): await establish_connection(FakeBleakClient, MagicMock(), "test") @pytest.mark.asyncio async def test_establish_connection_times_out(): class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): raise asyncio.TimeoutError() async def disconnect(self, *args, **kwargs): pass with ( patch("bleak_retry_connector.calculate_backoff_time", return_value=0), pytest.raises(BleakNotFoundError), ): await establish_connection(FakeBleakClient, MagicMock(), "test") @pytest.mark.asyncio async def test_establish_connection_has_transient_error(): attempts = 0 class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): nonlocal attempts attempts += 1 if attempts < MAX_TRANSIENT_ERRORS: raise BleakError("le-connection-abort-by-local") pass async def disconnect(self, *args, **kwargs): pass with patch("bleak_retry_connector.calculate_backoff_time", return_value=0): client = await establish_connection(FakeBleakClient, MagicMock(), "test") assert isinstance(client, FakeBleakClient) assert attempts == 9 @pytest.mark.asyncio async def test_establish_connection_has_transient_broken_pipe_error(): attempts = 0 class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): nonlocal attempts attempts += 1 if attempts < MAX_TRANSIENT_ERRORS: raise BrokenPipeError pass async def disconnect(self, *args, **kwargs): pass client = await establish_connection(FakeBleakClient, MagicMock(), "test") assert isinstance(client, FakeBleakClient) assert attempts == 9 @pytest.mark.asyncio async def test_establish_connection_services_changed(): attempts = 0 disconnect_calls = 0 clear_cache_calls = 0 class FakeBleakClient(BleakClientWithServiceCache): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): nonlocal attempts attempts += 1 if attempts < MAX_TRANSIENT_ERRORS: raise KeyError async def disconnect(self, *args, **kwargs): nonlocal disconnect_calls disconnect_calls += 1 async def clear_cache(self) -> bool: nonlocal clear_cache_calls clear_cache_calls += 1 return True client = await establish_connection(FakeBleakClient, MagicMock(), "test") assert isinstance(client, FakeBleakClient) assert attempts == 9 assert disconnect_calls == 8 assert clear_cache_calls == 8 @pytest.mark.asyncio async def test_establish_connection_has_transient_error_had_advice(): class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): raise BleakError("le-connection-abort-by-local") async def disconnect(self, *args, **kwargs): pass with patch("bleak_retry_connector.calculate_backoff_time", return_value=0): try: await establish_connection( FakeBleakClient, BLEDevice( "aa:bb:cc:dd:ee:ff", "name", {"path": "/org/bluez/hci2/dev_FA_23_9D_AA_45_46"}, -127, ), "test", ) except BleakError as e: exc = e assert isinstance(exc, BleakAbortedError) assert str(exc) == ( "test - aa:bb:cc:dd:ee:ff: " "Failed to connect after 9 attempt(s): " "le-connection-abort-by-local: " "Interference/range; " "External Bluetooth adapter w/extension may help; " "Extension cables reduce USB 3 port interference" ) @pytest.mark.asyncio async def test_establish_connection_out_of_slots_advice(): class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): raise BleakError("out of connection slots") async def disconnect(self, *args, **kwargs): pass with patch("bleak_retry_connector.calculate_backoff_time", return_value=0): try: await establish_connection( FakeBleakClient, BLEDevice( "aa:bb:cc:dd:ee:ff", "name", {"source": "esphome_proxy_1"}, -127 ), "test", ) except BleakError as e: exc = e assert isinstance(exc, BleakOutOfConnectionSlotsError) assert str(exc) == ( "test - aa:bb:cc:dd:ee:ff: Failed to connect after 9 attempt(s): " "out of connection slots: The proxy/adapter is " "out of connection slots or the device is no " "longer reachable; Add additional proxies " "(https://esphome.github.io/bluetooth-proxies/) near this device" ) @pytest.mark.asyncio async def test_device_disappeared_error(): class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): raise BleakError( '[org.freedesktop.DBus.Error.UnknownObject] Method "Connect" with ' 'signature "" on interface ' '"org.bluez.Device1" ' "doesn't exist" ) async def disconnect(self, *args, **kwargs): pass with patch("bleak_retry_connector.calculate_backoff_time", return_value=0): try: await establish_connection( FakeBleakClient, BLEDevice( "aa:bb:cc:dd:ee:ff", "name", {"path": "/org/bluez/hci2/dev_FA_23_9D_AA_45_46"}, -127, ), "test", ) except BleakError as e: exc = e assert isinstance(exc, BleakNotFoundError) assert str(exc) == ( "test - aa:bb:cc:dd:ee:ff: " "Failed to connect after 4 attempt(s): " "[org.freedesktop.DBus.Error.UnknownObject] " 'Method "Connect" with signature "" on interface "org.bluez.Device1" ' "doesn't exist: The device disappeared; " "Try restarting the scanner or moving the device closer" ) @pytest.mark.asyncio @patch.object(bleak_retry_connector.bluez, "IS_LINUX", True) async def test_device_disappeared_and_reappears(): class FakeBluezManager: def __init__(self): self.watchers: set[DeviceWatcher] = set() self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Connected": True, "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, } def add_device_watcher(self, path: str, **kwargs: Any) -> DeviceWatcher: """Add a watcher for device changes.""" watcher = DeviceWatcher(path, **kwargs) self.watchers.add(watcher) return watcher async def _wait_condition(self, *args: Any, **kwargs: Any) -> None: """Wait for a condition to be met.""" raise KeyError def remove_device_watcher(self, watcher: DeviceWatcher) -> None: """Remove a watcher for device changes.""" self.watchers.remove(watcher) def is_connected(self, path: str) -> bool: """Check if device is connected.""" return False bluez_manager = FakeBluezManager() bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=bluez_manager ) bleak_retry_connector.bluez.defs = defs class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): raise BleakDeviceNotFoundError( '[org.freedesktop.DBus.Error.UnknownObject] Method "Connect" with ' 'signature "" on interface ' '"org.bluez.Device1" ' "doesn't exist" ) async def disconnect(self, *args, **kwargs): pass with ( patch("bleak_retry_connector.calculate_backoff_time", return_value=0.01), patch.object(bleak_retry_connector.bluez, "REAPPEAR_WAIT_INTERVAL", 0.0025), ): try: await establish_connection( FakeBleakClient, BLEDevice( "FA:23:9D:AA:45:46", "name", {"path": "/org/bluez/hci2/dev_FA_23_9D_AA_45_46"}, -127, ), "test", ) except BleakError as e: exc = e assert isinstance(exc, BleakNotFoundError) assert str(exc) == ( "test - FA:23:9D:AA:45:46: " "Failed to connect after 9 attempt(s): " "BleakDeviceNotFoundError: " "The device disappeared; " "Try restarting the scanner or moving the device closer" ) @pytest.mark.asyncio async def test_establish_connection_has_one_unknown_error(): attempts = 0 class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): nonlocal attempts attempts += 1 if attempts == 1: raise BleakError("unknown") pass async def disconnect(self, *args, **kwargs): pass client = await establish_connection(FakeBleakClient, MagicMock(), "test") assert isinstance(client, FakeBleakClient) assert attempts == 2 @pytest.mark.asyncio async def test_establish_connection_has_one_many_error(): attempts = 0 class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): nonlocal attempts attempts += 1 if attempts < 10: raise BleakError("unknown") pass async def disconnect(self, *args, **kwargs): pass with ( patch("bleak_retry_connector.calculate_backoff_time", return_value=0), pytest.raises(BleakConnectionError), ): await establish_connection(FakeBleakClient, MagicMock(), "test") @pytest.mark.asyncio async def test_bleak_connect_overruns_timeout(): class FakeBleakClient(BleakClient): def __init__(self, *args, **kwargs): pass async def connect(self, *args, **kwargs): await asyncio.sleep(40) async def disconnect(self, *args, **kwargs): pass with ( patch("bleak_retry_connector.calculate_backoff_time", return_value=0), patch.object(bleak_retry_connector, "BLEAK_SAFETY_TIMEOUT", 0), pytest.raises(BleakNotFoundError), ): await establish_connection(FakeBleakClient, MagicMock(), "test") def test_ble_device_has_changed(): """Test that the BLEDevice has changed when the underlying device has changed.""" assert not ble_device_has_changed( BLEDevice("aa:bb:cc:dd:ee:ff", "name", {"path": "/dev/1"}, -127), BLEDevice("aa:bb:cc:dd:ee:ff", "name", {"path": "/dev/1"}, -127), ) assert ble_device_has_changed( BLEDevice("aa:bb:cc:dd:ee:ff", "name", {"path": "/dev/1"}, -127), BLEDevice("ab:bb:cc:dd:ee:ff", "name", {"path": "/dev/1"}, -127), ) assert ble_device_has_changed( BLEDevice("aa:bb:cc:dd:ee:ff", "name", {"path": "/dev/1"}, -127), BLEDevice("aa:bb:cc:dd:ee:ff", "name", {"path": "/dev/2"}, -127), ) @pytest.mark.asyncio async def test_establish_connection_other_adapter_already_connected(mock_linux): device: BLEDevice | None = None class FakeBleakClient(BleakClient): def __init__(self, ble_device_or_address, *args, **kwargs): ble_device_or_address.metadata["delegate"] = 0 super().__init__(ble_device_or_address, *args, **kwargs) nonlocal device device = ble_device_or_address self._device_path = "/org/bluez/hci2/dev_FA_23_9D_AA_45_46" async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" async def get_services(self, *args, **kwargs): return [] collection = BleakGATTServiceCollection() class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "Connected": True, "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "Connected": False, "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs client = await establish_connection( FakeBleakClientWithServiceCache, BLEDevice( "aa:bb:cc:dd:ee:ff", "name", {"path": "/org/bluez/hci2/dev_FA_23_9D_AA_45_46"}, -80, delegate=False, ), "test", disconnected_callback=MagicMock(), cached_services=collection, ) assert isinstance(client, FakeBleakClientWithServiceCache) await client.get_services() is collection assert device is not None assert device.details["path"] == "/org/bluez/hci1/dev_FA_23_9D_AA_45_46" @pytest.mark.asyncio async def test_establish_connection_device_disappeared(mock_linux): class FakeBleakClient(BleakClient): def __init__(self, ble_device_or_address, *args, **kwargs): ble_device_or_address.metadata["delegate"] = 0 super().__init__(ble_device_or_address, *args, **kwargs) self._device_path = "/org/bluez/hci2/dev_FA_23_9D_AA_45_46" async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" async def get_services(self, *args, **kwargs): return [] collection = BleakGATTServiceCollection() class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "bob", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs with patch("bleak_retry_connector.calculate_backoff_time", return_value=0): client = await establish_connection( FakeBleakClientWithServiceCache, BLEDevice( "aa:bb:cc:dd:ee:ff", "name", {"path": "/org/bluez/hci2/dev_FA_23_9D_AA_45_46"}, -127, delegate=False, ), "test", disconnected_callback=MagicMock(), cached_services=collection, ) assert isinstance(client, FakeBleakClientWithServiceCache) await client.get_services() is collection @pytest.mark.asyncio async def test_get_device(mock_linux): class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs device = await get_device("FA:23:9D:AA:45:46") assert device is not None assert device.details["path"] == "/org/bluez/hci0/dev_FA_23_9D_AA_45_46" @pytest.mark.asyncio async def test_clear_cache(mock_linux): class FakeBluezManager: def __init__(self): self._services_cache = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": "test", "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": "test", } self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } bluez_manager = FakeBluezManager() bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=bluez_manager ) bleak_retry_connector.bluez.defs = defs device = await get_device("FA:23:9D:AA:45:46") assert device is not None assert device.details["path"] == "/org/bluez/hci0/dev_FA_23_9D_AA_45_46" assert await clear_cache("FA:23:9D:AA:45:46") assert bluez_manager._services_cache == {} @pytest.mark.asyncio async def test_get_device_mac_os(mock_macos): class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs device = await get_device("FA:23:9D:AA:45:46") assert device is None @pytest.mark.asyncio async def test_get_device_already_connected(mock_linux): class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci1/dev_BD_24_6F_85_AA_61": { "org.freedesktop.DBus.Introspectable": {}, "org.bluez.Device1": { "Address": "BD:24:6F:85:AA:61", "AddressType": "public", "Name": "Dream~BD246F85AA61", "Alias": "Dream~BD246F85AA61", "Appearance": 962, "Icon": "input-mouse", "Paired": False, "Trusted": False, "Blocked": False, "LegacyPairing": False, "Connected": True, "UUIDs": [ "00001800-0000-1000-8000-00805f9b34fb", "00001801-0000-1000-8000-00805f9b34fb", "0000180a-0000-1000-8000-00805f9b34fb", "0000ffd0-0000-1000-8000-00805f9b34fb", "0000ffd5-0000-1000-8000-00805f9b34fb", ], "Modalias": "usb:v045Ep0040d0300", "Adapter": "/org/bluez/hci1", "ManufacturerData": {20808: bytearray(b"364656")}, "ServicesResolved": True, }, "org.freedesktop.DBus.Properties": {}, } } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs device = await get_device("BD:24:6F:85:AA:61") assert device is not None assert device.details["path"] == "/org/bluez/hci1/dev_BD_24_6F_85_AA_61" connected = await get_connected_devices(device) assert len(connected) == 1 assert isinstance(connected[0], BLEDevice) assert connected[0].details["path"] == "/org/bluez/hci1/dev_BD_24_6F_85_AA_61" @pytest.mark.asyncio async def test_get_device_not_there(): class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs with patch.object(bleak_retry_connector.const, "IS_LINUX", True): device = await get_device("00:00:00:00:00:00") assert device is None @pytest.mark.asyncio async def test_establish_connection_better_rssi_available_already_connected_supported_different_adapter( mock_linux, ): device: BLEDevice | None = None class FakeBleakClient(BleakClient): def __init__(self, ble_device_or_address, *args, **kwargs): ble_device_or_address.metadata["delegate"] = 0 super().__init__(ble_device_or_address, *args, **kwargs) nonlocal device device = ble_device_or_address self._device_path = "/org/bluez/hci2/dev_FA_23_9D_AA_45_46" async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" async def get_services(self, *args, **kwargs): return [] collection = BleakGATTServiceCollection() class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Connected": True, "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Connected": True, "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs mock_device = BLEDevice( "aa:bb:cc:dd:ee:ff", "name", {"path": "/org/bluez/hci2/dev_FA_23_9D_AA_45_46"}, -80, delegate=False, ) connected = await get_connected_devices(mock_device) assert len(connected) == 2 assert isinstance(connected[0], BLEDevice) assert connected[0].details["path"] == "/org/bluez/hci1/dev_FA_23_9D_AA_45_46" assert connected[1].details["path"] == "/org/bluez/hci2/dev_FA_23_9D_AA_45_46" with patch("bleak_retry_connector._disconnect_devices") as mock_disconnect_device: client = await establish_connection( FakeBleakClientWithServiceCache, BLEDevice( "FA:23:9D:AA:45:46", "name", {"path": "/org/bluez/hci2/dev_FA_23_9D_AA_45_46"}, -80, delegate=False, ), "test", disconnected_callback=MagicMock(), cached_services=collection, ) assert isinstance(client, FakeBleakClientWithServiceCache) await client.get_services() is collection assert device is not None assert device.details["path"] == "/org/bluez/hci1/dev_FA_23_9D_AA_45_46" assert not mock_disconnect_device.mock_calls @pytest.mark.asyncio async def test_establish_connection_better_rssi_available_already_connected_supported_same_adapter( mock_linux, ): device: BLEDevice | None = None class FakeBleakClient(BleakClient): def __init__(self, ble_device_or_address, *args, **kwargs): ble_device_or_address.metadata["delegate"] = 0 super().__init__(ble_device_or_address, *args, **kwargs) nonlocal device device = ble_device_or_address self._device_path = "/org/bluez/hci2/dev_FA_23_9D_AA_45_46" async def connect(self, *args, **kwargs): return True async def disconnect(self, *args, **kwargs): pass async def get_services(self, *args, **kwargs): return [] class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClient): """Fake BleakClientWithServiceCache.""" async def get_services(self, *args, **kwargs): return [] collection = BleakGATTServiceCollection() class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Connected": True, "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Connected": True, "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs mock_device = BLEDevice( "aa:bb:cc:dd:ee:ff", "name", {"path": "/org/bluez/hci2/dev_FA_23_9D_AA_45_46"}, -80, delegate=False, ) connected = await get_connected_devices(mock_device) assert len(connected) == 2 assert isinstance(connected[0], BLEDevice) assert connected[0].details["path"] == "/org/bluez/hci1/dev_FA_23_9D_AA_45_46" assert connected[1].details["path"] == "/org/bluez/hci2/dev_FA_23_9D_AA_45_46" with ( patch("bleak_retry_connector._disconnect_devices") as mock_disconnect_device, patch("bleak.get_platform_client_backend_type"), ): client = await establish_connection( FakeBleakClientWithServiceCache, BLEDevice( "FA:23:9D:AA:45:46", "name", {"path": "/org/bluez/hci1/dev_FA_23_9D_AA_45_46"}, -80, delegate=False, ), "test", disconnected_callback=MagicMock(), cached_services=collection, ) assert isinstance(client, FakeBleakClientWithServiceCache) await client.get_services() is collection assert device is not None assert device.details["path"] == "/org/bluez/hci1/dev_FA_23_9D_AA_45_46" assert not mock_disconnect_device.mock_calls @pytest.mark.asyncio async def test_get_device_by_adapter(mock_linux): class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) bleak_retry_connector.bluez.defs = defs device_hci0 = await get_device_by_adapter("FA:23:9D:AA:45:46", "hci0") device_hci1 = await get_device_by_adapter("FA:23:9D:AA:45:46", "hci1") assert device_hci0 is not None assert device_hci0.details["path"] == "/org/bluez/hci0/dev_FA_23_9D_AA_45_46" assert device_hci1 is not None assert device_hci1.details["path"] == "/org/bluez/hci1/dev_FA_23_9D_AA_45_46" def test_calculate_backoff_time(): """Test that the backoff time is calculated correctly.""" assert calculate_backoff_time(Exception()) == BLEAK_BACKOFF_TIME assert ( calculate_backoff_time(BleakDBusError(MagicMock(), MagicMock())) == BLEAK_DBUS_BACKOFF_TIME ) assert ( calculate_backoff_time( BleakError( "No backend with an available connection slot that can reach address EB:4A:D4:93:68:EF was found" ) ) == BLEAK_OUT_OF_SLOTS_BACKOFF_TIME ) assert ( calculate_backoff_time(BleakError("ESP_GATT_CONN_TERMINATE_PEER_USER")) == BLEAK_TRANSIENT_BACKOFF_TIME ) assert ( calculate_backoff_time(BleakError("ESP_GATT_CONN_FAIL_ESTABLISH")) == BLEAK_TRANSIENT_MEDIUM_BACKOFF_TIME ) assert ( calculate_backoff_time(BleakError("ESP_GATT_ERROR")) == BLEAK_TRANSIENT_LONG_BACKOFF_TIME ) assert ( calculate_backoff_time(BleakDeviceNotFoundError("Out of slots")) == BLEAK_OUT_OF_SLOTS_BACKOFF_TIME ) @pytest.mark.asyncio async def test_retry_bluetooth_connection_error(): """Test that the retry_bluetooth_connection_error decorator works correctly.""" @retry_bluetooth_connection_error() async def test_function(): raise BleakDBusError(MagicMock(), MagicMock()) with patch( "bleak_retry_connector.calculate_backoff_time" ) as mock_calculate_backoff_time: mock_calculate_backoff_time.return_value = 0 with pytest.raises(BleakDBusError): await test_function() assert mock_calculate_backoff_time.call_count == 2 @pytest.mark.asyncio async def test_retry_bluetooth_connection_error_non_default_max_attempts(): """Test that the retry_bluetooth_connection_error decorator works correctly with a different number of retries.""" @retry_bluetooth_connection_error(4) async def test_function(): raise BleakDBusError(MagicMock(), MagicMock()) with patch( "bleak_retry_connector.calculate_backoff_time" ) as mock_calculate_backoff_time: mock_calculate_backoff_time.return_value = 0 with pytest.raises(BleakDBusError): await test_function() assert mock_calculate_backoff_time.call_count == 4 @pytest.mark.asyncio async def test_dbus_is_missing(mock_linux): """Test getting a device when dbus is missing.""" bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( side_effect=FileNotFoundError("dbus not here") ) bleak_retry_connector.bluez.defs = defs with patch.object(bleak_retry_connector.const, "IS_LINUX", True): device = await get_device("FA:23:9D:AA:45:46") assert device is None class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) device = await get_device("FA:23:9D:AA:45:46") assert device is None _reset_dbus_socket_cache() device = await get_device("FA:23:9D:AA:45:46") assert device is not None @pytest.mark.asyncio async def test_ble_device_description(): device = BLEDevice( "aa:bb:cc:dd:ee:ff", "name", {"path": "/org/bluez/hci2/dev_FA_23_9D_AA_45_46"}, -127, ) assert ( ble_device_description(device) == "aa:bb:cc:dd:ee:ff - name -> /org/bluez/hci2" ) device2 = BLEDevice( "aa:bb:cc:dd:ee:ff", "name", {"path": "/org/bluez/hci2/dev_FA_23_9D_AA_45_46"}, -127, ) assert ( ble_device_description(device2) == "aa:bb:cc:dd:ee:ff - name -> /org/bluez/hci2" ) device3 = BLEDevice( "aa:bb:cc:dd:ee:ff", "name", {"source": "esphome_proxy_1"}, -127 ) assert ( ble_device_description(device3) == "aa:bb:cc:dd:ee:ff - name -> esphome_proxy_1" ) @pytest.mark.asyncio @pytest.mark.skipif("not bleak_retry_connector.const.IS_LINUX") async def test_restore_discoveries(mock_linux): class FakeBluezManager: def __init__(self): self._properties = { "/org/bluez/hci1/dev_BD_24_6F_85_AA_61": { "org.freedesktop.DBus.Introspectable": {}, "org.bluez.Device1": { "Address": "BD:24:6F:85:AA:61", "AddressType": "public", "Name": "Dream~BD246F85AA61", "Alias": "Dream~BD246F85AA61", "Appearance": 962, "Icon": "input-mouse", "Paired": False, "Trusted": False, "Blocked": False, "LegacyPairing": False, "Connected": True, "UUIDs": [ "00001800-0000-1000-8000-00805f9b34fb", "00001801-0000-1000-8000-00805f9b34fb", "0000180a-0000-1000-8000-00805f9b34fb", "0000ffd0-0000-1000-8000-00805f9b34fb", "0000ffd5-0000-1000-8000-00805f9b34fb", ], "Modalias": "usb:v045Ep0040d0300", "Adapter": "/org/bluez/hci1", "ManufacturerData": {20808: bytearray(b"364656")}, "ServicesResolved": True, }, "org.freedesktop.DBus.Properties": {}, }, "/org/bluez/hci5/dev_BE_24_6F_85_AA_61": { "org.freedesktop.DBus.Introspectable": {}, "org.bluez.Device1": { "Address": "BE:24:6F:85:AA:61", "AddressType": "public", "Name": "Dream~BD246F85AA61", "Alias": "Dream~BD246F85AA61", "Appearance": 962, "Icon": "input-mouse", "Paired": False, "Trusted": False, "Blocked": False, "LegacyPairing": False, "Connected": True, "UUIDs": [ "00001800-0000-1000-8000-00805f9b34fb", "00001801-0000-1000-8000-00805f9b34fb", "0000180a-0000-1000-8000-00805f9b34fb", "0000ffd0-0000-1000-8000-00805f9b34fb", "0000ffd5-0000-1000-8000-00805f9b34fb", ], "Modalias": "usb:v045Ep0040d0300", "Adapter": "/org/bluez/hci1", "ManufacturerData": {20808: bytearray(b"364656")}, "ServicesResolved": True, }, "org.freedesktop.DBus.Properties": {}, }, } bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=FakeBluezManager() ) from bluetooth_adapters.history import load_history_from_managed_objects bleak_retry_connector.load_history_from_managed_objects = ( load_history_from_managed_objects ) bleak_retry_connector.bluez.defs = defs seen_devices: dict[str, tuple[BLEDevice, AdvertisementData]] = {} mock_backend = Mock(seen_devices=seen_devices) mock_scanner = Mock(_backend=mock_backend) await restore_discoveries(mock_scanner, "hci1") assert len(seen_devices) == 1 @pytest.mark.asyncio async def test_close_stale_connections_by_address(mock_linux): class FakeBluezManager: def __init__(self): self._services_cache = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": "test", "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": "test", } self._properties = { "/org/bluez/hci0/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -30, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci1/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -79, "Connected": True, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci2/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -80, }, defs.GATT_SERVICE_INTERFACE: True, }, "/org/bluez/hci3/dev_FA_23_9D_AA_45_46": { "UUID": "service", "Primary": True, "Characteristics": [], defs.DEVICE_INTERFACE: { "Address": "FA:23:9D:AA:45:46", "Alias": "FA:23:9D:AA:45:46", "RSSI": -31, }, defs.GATT_SERVICE_INTERFACE: True, }, } bluez_manager = FakeBluezManager() bleak_retry_connector.bleak_manager.get_global_bluez_manager = AsyncMock( return_value=bluez_manager ) bleak_retry_connector.bluez.defs = defs with patch.object( bleak_retry_connector, "disconnect_devices", AsyncMock() ) as mock_disconnect_devices: await close_stale_connections_by_address("FA:23:9D:AA:45:46") assert len(mock_disconnect_devices.mock_calls) == 1