pax_global_header 0000666 0000000 0000000 00000000064 14773036752 0014530 g ustar 00root root 0000000 0000000 52 comment=4f8c1837df93e09ac6e23755fe4493d5c809ee63
bleak-retry-connector-3.10.0/ 0000775 0000000 0000000 00000000000 14773036752 0016022 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/.all-contributorsrc 0000664 0000000 0000000 00000000474 14773036752 0021660 0 ustar 00root root 0000000 0000000 {
"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/.editorconfig 0000664 0000000 0000000 00000000444 14773036752 0020501 0 ustar 00root root 0000000 0000000 # 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/.flake8 0000664 0000000 0000000 00000000056 14773036752 0017176 0 ustar 00root root 0000000 0000000 [flake8]
exclude = docs
max-line-length = 120
bleak-retry-connector-3.10.0/.github/ 0000775 0000000 0000000 00000000000 14773036752 0017362 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14773036752 0021545 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/.github/ISSUE_TEMPLATE/1-bug_report.md 0000664 0000000 0000000 00000000422 14773036752 0024373 0 ustar 00root root 0000000 0000000 ---
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.md 0000664 0000000 0000000 00000000672 14773036752 0025354 0 ustar 00root root 0000000 0000000 ---
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.yml 0000664 0000000 0000000 00000001343 14773036752 0022213 0 ustar 00root root 0000000 0000000 # 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.toml 0000664 0000000 0000000 00000003515 14773036752 0021525 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 14773036752 0021417 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/.github/workflows/ci.yml 0000664 0000000 0000000 00000004727 14773036752 0022547 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000534 14773036752 0024770 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000001340 14773036752 0024700 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000775 14773036752 0023415 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000004066 14773036752 0020020 0 ustar 00root root 0000000 0000000 # 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.yml 0000664 0000000 0000000 00000000306 14773036752 0020110 0 ustar 00root root 0000000 0000000 tasks:
- 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/ 0000775 0000000 0000000 00000000000 14773036752 0017002 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/.idea/bleak-retry-connector.iml 0000664 0000000 0000000 00000000515 14773036752 0023717 0 ustar 00root root 0000000 0000000
bleak-retry-connector-3.10.0/.idea/watcherTasks.xml 0000664 0000000 0000000 00000005253 14773036752 0022174 0 ustar 00root root 0000000 0000000
bleak-retry-connector-3.10.0/.idea/workspace.xml 0000664 0000000 0000000 00000002741 14773036752 0021526 0 ustar 00root root 0000000 0000000
bleak-retry-connector-3.10.0/.pre-commit-config.yaml 0000664 0000000 0000000 00000003416 14773036752 0022307 0 ustar 00root root 0000000 0000000 # 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.yml 0000664 0000000 0000000 00000001005 14773036752 0021104 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000137636 14773036752 0017653 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000007502 14773036752 0020257 0 ustar 00root root 0000000 0000000 # 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/LICENSE 0000664 0000000 0000000 00000002060 14773036752 0017025 0 ustar 00root root 0000000 0000000
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.md 0000664 0000000 0000000 00000007137 14773036752 0017311 0 ustar 00root root 0000000 0000000 # Bleak Retry Connector
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.mjs 0000664 0000000 0000000 00000000362 14773036752 0022161 0 ustar 00root root 0000000 0000000 export 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/ 0000775 0000000 0000000 00000000000 14773036752 0016752 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/docs/Makefile 0000664 0000000 0000000 00000001175 14773036752 0020416 0 ustar 00root root 0000000 0000000 # 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.bat 0000664 0000000 0000000 00000001374 14773036752 0020364 0 ustar 00root root 0000000 0000000 @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/ 0000775 0000000 0000000 00000000000 14773036752 0020252 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/docs/source/_static/ 0000775 0000000 0000000 00000000000 14773036752 0021700 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/docs/source/_static/.gitkeep 0000664 0000000 0000000 00000000000 14773036752 0023317 0 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/docs/source/changelog.md 0000664 0000000 0000000 00000000045 14773036752 0022522 0 ustar 00root root 0000000 0000000 ```{include} ../../CHANGELOG.md
```
bleak-retry-connector-3.10.0/docs/source/conf.py 0000664 0000000 0000000 00000003657 14773036752 0021564 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000000050 14773036752 0023276 0 ustar 00root root 0000000 0000000 ```{include} ../../CONTRIBUTING.md
```
bleak-retry-connector-3.10.0/docs/source/index.md 0000664 0000000 0000000 00000000365 14773036752 0021707 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000000300 14773036752 0023266 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000000153 14773036752 0021677 0 ustar 00root root 0000000 0000000 # Usage
To use this package, import it:
```python
import bleak_retry_connector
```
TODO: Document usage
bleak-retry-connector-3.10.0/poetry.lock 0000664 0000000 0000000 00000203446 14773036752 0020227 0 ustar 00root root 0000000 0000000 # 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.toml 0000664 0000000 0000000 00000005034 14773036752 0020740 0 ustar 00root root 0000000 0000000 [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.json 0000664 0000000 0000000 00000000101 14773036752 0020530 0 ustar 00root root 0000000 0000000 {
"extends": ["github>browniebroke/renovate-configs:python"]
}
bleak-retry-connector-3.10.0/setup.py 0000664 0000000 0000000 00000000373 14773036752 0017537 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 14773036752 0016611 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/src/bleak_retry_connector/ 0000775 0000000 0000000 00000000000 14773036752 0023166 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/src/bleak_retry_connector/__init__.py 0000664 0000000 0000000 00000044701 14773036752 0025305 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000004364 14773036752 0026317 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000046006 14773036752 0024667 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000000333 14773036752 0024665 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000002531 14773036752 0024476 0 ustar 00root root 0000000 0000000 from __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.typed 0000664 0000000 0000000 00000000000 14773036752 0024653 0 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/src/bleak_retry_connector/util.py 0000664 0000000 0000000 00000000430 14773036752 0024512 0 ustar 00root root 0000000 0000000 from __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/ 0000775 0000000 0000000 00000000000 14773036752 0017164 5 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/tests/__init__.py 0000664 0000000 0000000 00000000000 14773036752 0021263 0 ustar 00root root 0000000 0000000 bleak-retry-connector-3.10.0/tests/conftest.py 0000664 0000000 0000000 00000001504 14773036752 0021363 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000050310 14773036752 0021715 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000177326 14773036752 0021560 0 ustar 00root root 0000000 0000000 from __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