pax_global_header00006660000000000000000000000064147734333250014525gustar00rootroot0000000000000052 comment=70f5bd9253ab79fdad8c2580d3b82d6aa55143cc bthome-ble-3.12.5/000077500000000000000000000000001477343332500136335ustar00rootroot00000000000000bthome-ble-3.12.5/.all-contributorsrc000066400000000000000000000004611477343332500174650ustar00rootroot00000000000000{ "projectName": "bthome-ble", "projectOwner": "bluetooth-devices", "repoType": "github", "repoHost": "https://github.com", "files": [ "README.md" ], "imageSize": 80, "commit": true, "commitConvention": "angular", "contributors": [], "contributorsPerLine": 7, "skipCi": true } bthome-ble-3.12.5/.editorconfig000066400000000000000000000004441477343332500163120ustar00rootroot00000000000000# 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 bthome-ble-3.12.5/.flake8000066400000000000000000000000561477343332500150070ustar00rootroot00000000000000[flake8] exclude = docs max-line-length = 100 bthome-ble-3.12.5/.github/000077500000000000000000000000001477343332500151735ustar00rootroot00000000000000bthome-ble-3.12.5/.github/ISSUE_TEMPLATE/000077500000000000000000000000001477343332500173565ustar00rootroot00000000000000bthome-ble-3.12.5/.github/ISSUE_TEMPLATE/1-bug_report.md000066400000000000000000000004221477343332500222040ustar00rootroot00000000000000--- 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. bthome-ble-3.12.5/.github/ISSUE_TEMPLATE/2-feature-request.md000066400000000000000000000006721477343332500231650ustar00rootroot00000000000000--- 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. bthome-ble-3.12.5/.github/dependabot.yml000066400000000000000000000013441477343332500200250ustar00rootroot00000000000000# 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: "monthly" 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" bthome-ble-3.12.5/.github/labels.toml000066400000000000000000000035151477343332500173360ustar00rootroot00000000000000[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" bthome-ble-3.12.5/.github/workflows/000077500000000000000000000000001477343332500172305ustar00rootroot00000000000000bthome-ble-3.12.5/.github/workflows/ci.yml000066400000000000000000000043401477343332500203470ustar00rootroot00000000000000name: 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.13" - 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.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 run: poetry install - name: Test with Pytest 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 permissions: id-token: write contents: write if: github.ref == 'refs/heads/main' needs: - test - lint - commitlint steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Python Semantic Release id: release uses: python-semantic-release/python-semantic-release@v9.21.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 if: steps.release.outputs.released == 'true' - name: Publish package to GitHub Release uses: python-semantic-release/publish-action@v9.21.0 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} bthome-ble-3.12.5/.github/workflows/hacktoberfest.yml000066400000000000000000000005341477343332500226010ustar00rootroot00000000000000name: 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 }} bthome-ble-3.12.5/.github/workflows/issue-manager.yml000066400000000000000000000013401477343332500225110ustar00rootroot00000000000000name: 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." } } bthome-ble-3.12.5/.github/workflows/labels.yml000066400000000000000000000007751477343332500212260ustar00rootroot00000000000000name: 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.12 - 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 bthome-ble-3.12.5/.gitignore000066400000000000000000000040661477343332500156310ustar00rootroot00000000000000# 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/ bthome-ble-3.12.5/.gitpod.yml000066400000000000000000000003061477343332500157210ustar00rootroot00000000000000tasks: - command: | pip install poetry PIP_USER=false poetry install - command: | pip install pre-commit pre-commit install PIP_USER=false pre-commit install-hooks bthome-ble-3.12.5/.idea/000077500000000000000000000000001477343332500146135ustar00rootroot00000000000000bthome-ble-3.12.5/.idea/bthome-ble.iml000066400000000000000000000005151477343332500173350ustar00rootroot00000000000000 bthome-ble-3.12.5/.idea/watcherTasks.xml000066400000000000000000000052531477343332500200050ustar00rootroot00000000000000 bthome-ble-3.12.5/.idea/workspace.xml000066400000000000000000000027301477343332500173350ustar00rootroot00000000000000 bthome-ble-3.12.5/.pre-commit-config.yaml000066400000000000000000000032311477343332500201130ustar00rootroot00000000000000# 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: v3.1.0 # hooks: # - id: prettier - repo: https://github.com/asottile/pyupgrade rev: v3.19.1 hooks: - id: pyupgrade args: [--py311-plus] - repo: https://github.com/PyCQA/isort rev: 6.0.1 hooks: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror 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 - repo: https://github.com/PyCQA/bandit rev: 1.8.3 hooks: - id: bandit args: [-x, tests] bthome-ble-3.12.5/.readthedocs.yml000066400000000000000000000010041477343332500167140ustar00rootroot00000000000000# 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-20.04 tools: python: "3.9" # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . extra_requirements: - docs bthome-ble-3.12.5/CHANGELOG.md000066400000000000000000001031171477343332500154470ustar00rootroot00000000000000# Changelog ## v3.12.5 (2025-04-03) ### Bug fixes - Replace python-semantic-release/upload-to-gh-release with python-semantic-release/publish-action (#211) ([`412545f`](https://github.com/Bluetooth-Devices/bthome-ble/commit/412545f38c2a847b6c0a71a950c18528406d097b)) ### Chores - Update deps (#212) ([`8b966ee`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8b966ee10444b9dffca84f32cf06e13dd824466e)) - Pre-commit autoupdate (#210) ([`05685d5`](https://github.com/Bluetooth-Devices/bthome-ble/commit/05685d50c755af654b54d26dfa33ef888244946a)) - Pre-commit autoupdate (#208) ([`ca34d81`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ca34d8172e5a7bd4de8642ba5a9f134e48be7e9b)) - Bump python-semantic-release/python-semantic-release from 9.17.0 to 9.21.0 in the github-actions group (#207) ([`8679b94`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8679b942a1ea188e0168194ee4bc32462e34f251)) - Pre-commit autoupdate (#206) ([`f011cb3`](https://github.com/Bluetooth-Devices/bthome-ble/commit/f011cb3d4ffb93dfbe25835ed7d9da6eb00082b7)) - Pre-commit autoupdate (#205) ([`d73e81a`](https://github.com/Bluetooth-Devices/bthome-ble/commit/d73e81a20624aaf825336aa65284c6b0efeb91af)) - Pre-commit autoupdate (#204) ([`031f55f`](https://github.com/Bluetooth-Devices/bthome-ble/commit/031f55faf9b720094942bcf001f22f6706585ab7)) ## v3.12.4 (2025-02-05) ### Bug fixes - Update poetry to v2 (#203) ([`cd6cf77`](https://github.com/Bluetooth-Devices/bthome-ble/commit/cd6cf77f75db1b43a4e9fde215e9ee97000d8884)) ### Chores - Pre-commit autoupdate (#202) ([`c69055f`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c69055fab999304246d29f565d435f241f846b6c)) - Bump bluetooth-data-tools from 1.22.0 to 1.23.3 (#200) ([`a8e2428`](https://github.com/Bluetooth-Devices/bthome-ble/commit/a8e242850636980f8f98190f19b3aa6260e6b18a)) - Bump myst-parser from 3.0.1 to 4.0.0 (#201) ([`167b88a`](https://github.com/Bluetooth-Devices/bthome-ble/commit/167b88a1d4aeb54be0badfbc48aeda56b0387be8)) - Bump habluetooth from 3.17.0 to 3.21.0 (#199) ([`b64b747`](https://github.com/Bluetooth-Devices/bthome-ble/commit/b64b74729ad31d85b10f92163a0f25cc8956be12)) ## v3.12.3 (2025-01-31) ### Bug fixes - Update pyproject to allow python 3.13 deps (#198) ([`4c4d9a6`](https://github.com/Bluetooth-Devices/bthome-ble/commit/4c4d9a64be817fad5074aacd90d762e64f843845)) ## v3.12.2 (2025-01-31) ### Bug fixes - Use little endian in float conversion (#197) ([`ffd8f3c`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ffd8f3c6d959017bc662160cf345d9d49dd60f02)) ## v3.12.1 (2025-01-31) ### Bug fixes - Add changelog template (#196) ([`7ba1380`](https://github.com/Bluetooth-Devices/bthome-ble/commit/7ba138083f8cbf578b7c947fdf0320b29e5173cb)) ### Chores - Bump the github-actions group across 1 directory with 7 updates (#195) ([`3bf607e`](https://github.com/Bluetooth-Devices/bthome-ble/commit/3bf607e24585e262aa8bba4b071f47f4c18c4bc7)) - Bump habluetooth from 3.12.0 to 3.15.0 (#194) ([`e23edab`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e23edabef18641e4b6e2594ceeb451fb67c65b56)) - Bump sphinx from 6.2.1 to 7.4.7 (#193) ([`ebf9c3b`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ebf9c3bae5dbca4c5946a0bff541f40e3faa7a38)) - Update lint and commitlint (#190) ([`33b4e85`](https://github.com/Bluetooth-Devices/bthome-ble/commit/33b4e855e9e822da366175d1e1c2cbe4637bdc5a)) - Update dependabot.yml to include gha (#191) ([`e73268c`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e73268c84ce99fe8795ce38664242a4e3925eaa9)) ### Unknown ## v3.12.0 (2025-01-31) ### Features - Add direction and precipitation support (#187) ([`8c0df91`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8c0df91b7647b3fc2985edfd2382199f4d321665)) ### Chores - Pre-commit autoupdate (#185) ([`1e9f738`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1e9f738b457762e0e4d4eb232e73b077abe10c69)) - Bump myst-parser from 1.0.0 to 3.0.1 (#181) ([`8ce9658`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8ce9658095518e8fc0450377527e07e5813355ee)) - Bump habluetooth from 3.9.2 to 3.12.0 (#182) ([`59d6173`](https://github.com/Bluetooth-Devices/bthome-ble/commit/59d61738228bf7abac0b2e2f8d0081c981698412)) - Bump bluetooth-data-tools from 1.19.0 to 1.22.0 (#183) ([`39e3391`](https://github.com/Bluetooth-Devices/bthome-ble/commit/39e33911a4db85de1e1fe4e0423554395dbe067c)) - Bump sphinx-rtd-theme from 2.0.0 to 3.0.2 (#184) ([`0c756de`](https://github.com/Bluetooth-Devices/bthome-ble/commit/0c756de2f7cf405b7bcdadd4745694274a133fce)) - Bump sphinx from 5.3.0 to 6.2.1 (#177) ([`38eea52`](https://github.com/Bluetooth-Devices/bthome-ble/commit/38eea524022fedbd86106625ce55d673b979f30c)) - Bump pytest-cov from 4.1.0 to 6.0.0 (#179) ([`a694d85`](https://github.com/Bluetooth-Devices/bthome-ble/commit/a694d851cfae703c61b03f4e49b67d7b544a8027)) - Bump habluetooth from 3.9.0 to 3.9.2 (#178) ([`ab590ff`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ab590ff022c3594d70d597eebfa1d5415a817970)) - Bump cryptography from 43.0.1 to 44.0.0 (#175) ([`ebd8e9e`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ebd8e9e4d8140c4ecbc43ac0cc2cbffd1d01c2f0)) - Bump pytest from 7.4.4 to 8.3.4 (#176) ([`07e6dad`](https://github.com/Bluetooth-Devices/bthome-ble/commit/07e6dadb58c9f45aaf1f419adcef3f9932274ffa)) - Bump jinja2 from 3.1.4 to 3.1.5 (#172) ([`4fcc1a1`](https://github.com/Bluetooth-Devices/bthome-ble/commit/4fcc1a1aa8d349e614934580f7a631dc12886095)) - Bump cryptography from 42.0.8 to 43.0.1 (#171) ([`bdd449b`](https://github.com/Bluetooth-Devices/bthome-ble/commit/bdd449b68b1865ead298f5a105d53f6afcf51855)) - Bump certifi from 2024.6.2 to 2024.7.4 (#173) ([`4bf94dc`](https://github.com/Bluetooth-Devices/bthome-ble/commit/4bf94dcda236145bfdf30ee468e69459ea4ccc72)) - Bump urllib3 from 2.2.1 to 2.2.2 (#174) ([`77e5493`](https://github.com/Bluetooth-Devices/bthome-ble/commit/77e5493249efe6bb552f8ad98722afcff3d7cbf4)) - Bump myst-parser from 0.18.1 to 1.0.0 (#168) ([`fa3964d`](https://github.com/Bluetooth-Devices/bthome-ble/commit/fa3964df716baa1f1840b96cf20479e474a1a9d5)) - Bump bluetooth-sensor-state-data from 1.7.0 to 1.7.1 (#166) ([`d1a0f3a`](https://github.com/Bluetooth-Devices/bthome-ble/commit/d1a0f3aac30eab6f8ba4c27f5d371f00aa24355f)) - Update for python 3.13 (#165) ([`8361b69`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8361b6901f30066634f2a32c987affd42b32b832)) - Bump sensor-state-data from 2.18.0 to 2.18.1 (#167) ([`4522dc8`](https://github.com/Bluetooth-Devices/bthome-ble/commit/4522dc8efd66eb4ed2f259f1f9a3ee071d55538d)) - Bump sphinx-rtd-theme from 1.3.0 to 2.0.0 (#169) ([`481ff63`](https://github.com/Bluetooth-Devices/bthome-ble/commit/481ff63bde979f7070755bb2d7174b5bcf4da940)) - Bump habluetooth from 3.1.1 to 3.9.0 (#170) ([`9d6d360`](https://github.com/Bluetooth-Devices/bthome-ble/commit/9d6d36030281fa4ba6fc3764775663c21b9e52de)) - Create dependabot.yml ([`a2988df`](https://github.com/Bluetooth-Devices/bthome-ble/commit/a2988df1f355e887ff4b4a95dccd4b06f92b2200)) - Pre-commit autoupdate (#164) ([`0d22035`](https://github.com/Bluetooth-Devices/bthome-ble/commit/0d2203590b4d524e27153351e8ad3e148b8490cb)) - Pre-commit autoupdate (#163) ([`ff18462`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ff184624bed27311394863640643a491e25b715c)) - Pre-commit autoupdate (#160) ([`da60d99`](https://github.com/Bluetooth-Devices/bthome-ble/commit/da60d994355f8c0b2715753600fa791a46a6eb2d)) - Pre-commit autoupdate (#158) ([`c07015a`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c07015aa64b1d6997fca27871a6a19060d66a1c5)) - Pre-commit autoupdate (#157) ([`354359a`](https://github.com/Bluetooth-Devices/bthome-ble/commit/354359a0b59c4bd7c6c621341c089238f8a03993)) - Pre-commit autoupdate (#155) ([`f1216c3`](https://github.com/Bluetooth-Devices/bthome-ble/commit/f1216c3e338a67663d1436207c9db16e38f23d18)) - Pre-commit autoupdate (#154) ([`6f550fa`](https://github.com/Bluetooth-Devices/bthome-ble/commit/6f550fae80fc75677c2adfed57d04d5b2d6e9966)) - Pre-commit autoupdate (#151) ([`c165c5e`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c165c5e25ee554ce631227c744a3ceb7d5665b5c)) - Pre-commit autoupdate (#150) ([`ef50709`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ef50709e00d0b6506ddda30f5eb203f63e012bae)) - Add ohf logo to readme (#149) ([`5cc4215`](https://github.com/Bluetooth-Devices/bthome-ble/commit/5cc42155a7e90b0ce73c05312304801554a02212)) - Pre-commit autoupdate (#145) ([`e1b850d`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e1b850dcc1c68ffb10179a6796c7bd4f9d121eeb)) ### Unknown ## v3.11.0 (2024-08-25) ### Features - Add new sensors (#144) ([`25e87a0`](https://github.com/Bluetooth-Devices/bthome-ble/commit/25e87a0e2d7bb0109e148046ed09235a0d3fa596)) ## v3.10.0 (2024-08-23) ### Features - Add 0x56 for 16-bit conductivity measurement type (#142) ([`444eaca`](https://github.com/Bluetooth-Devices/bthome-ble/commit/444eacafb280fffe8ee28abc4e9236b84db15bf6)) ### Chores - Pre-commit autoupdate (#140) ([`07f4729`](https://github.com/Bluetooth-Devices/bthome-ble/commit/07f4729813086007e04a7c7dd77e7dc240433565)) - Pre-commit autoupdate (#139) ([`e6ab18b`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e6ab18b702735465207df952af1273762081d8e1)) - Pre-commit autoupdate (#137) ([`8305ab4`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8305ab41118aee608b6d5f63453bf1d76dbcae61)) - Pre-commit autoupdate (#136) ([`8de8a22`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8de8a22a154f2228161fdd6d1c9a861004a18148)) - Pre-commit autoupdate (#132) ([`8171fd4`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8171fd41955a8a9a89094c235f8213af1a6b2014)) ## v3.9.3 (2024-06-07) ### Bug fixes - Allow contents permission to workflow (#131) ([`eda9bfd`](https://github.com/Bluetooth-Devices/bthome-ble/commit/eda9bfd454af3a0fd58d11fd951bebc28374e89d)) - Rename pypi token (#130) ([`03a9d90`](https://github.com/Bluetooth-Devices/bthome-ble/commit/03a9d90c33582b773f0737daf95495b9d123bafd)) - Use list for version_toml (#129) ([`0fc3d45`](https://github.com/Bluetooth-Devices/bthome-ble/commit/0fc3d453d8b31723668daf6720c80adf71821b9c)) - Reference to gh action pypi push (#128) ([`88b1fe9`](https://github.com/Bluetooth-Devices/bthome-ble/commit/88b1fe968ee3f9e62bf2e1c49d445db079013235)) - Fix pypi workflow (#127) ([`53c8e0d`](https://github.com/Bluetooth-Devices/bthome-ble/commit/53c8e0dc34af2ef86f4fbc67617dc35a3d951962)) - Fix release workflow (#126) ([`4d985e9`](https://github.com/Bluetooth-Devices/bthome-ble/commit/4d985e91edfb01be2df83b6074758e88b288da39)) - Bump python-semantic-release (#124) ([`bbef3a7`](https://github.com/Bluetooth-Devices/bthome-ble/commit/bbef3a7f7ba4b77949a60cbaba6d2e64889e8b1b)) ## v3.9.2 (2024-06-07) ### Bug fixes - Make tests compatible with habluetooth>=3.0 (#123) ([`92fceeb`](https://github.com/Bluetooth-Devices/bthome-ble/commit/92fceebf97add809f883039d9954f35f61a17019)) ## v3.9.1 (2024-06-05) ### Bug fixes - Accept new encryption counter only if the message was not corrupted (#119) ([`bc61824`](https://github.com/Bluetooth-Devices/bthome-ble/commit/bc61824bfe6c1d3c7394e7754f757a0241c14939)) ## v3.9.0 (2024-05-07) ### Features - Add button hold event (#120) ([`49124ab`](https://github.com/Bluetooth-Devices/bthome-ble/commit/49124ab9db18e179aba9fb731ba4ed08e113eff3)) ## v3.8.1 (2024-03-18) ### Bug fixes - Remove mac workaround (#115) ([`303f833`](https://github.com/Bluetooth-Devices/bthome-ble/commit/303f8331dac612f848c875869284be0ca621692e)) ## v3.8.0 (2024-03-10) ### Features - Allow one failed decryption before reauth (#114) ([`7c499cc`](https://github.com/Bluetooth-Devices/bthome-ble/commit/7c499cce6c15ab6f21b25779bae773e3c00f7841)) ## v3.7.0 (2024-03-08) ### Features - Add device title to log messages (#113) ([`c47f36f`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c47f36f17698ad48a94db3501a37ae9421c7ff7e)) ## v3.6.0 (2024-03-02) ### Features - Verify packet is not an older packet (#112) ([`113e49d`](https://github.com/Bluetooth-Devices/bthome-ble/commit/113e49df1460ea407202e5ca93ff170d6b813b75)) ## v3.5.0 (2024-01-18) ### Features - Add verification of the packet id (#105) ([`c428f92`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c428f927b8dbc4f02ef006c15932aca5fa74877f)) ## v3.4.2 (2024-01-17) ### Bug fixes - Fix counter verification (#104) ([`dead672`](https://github.com/Bluetooth-Devices/bthome-ble/commit/dead67297a2a5662d6a9b12fcc8e671ae1d833a1)) ## v3.4.1 (2024-01-10) ### Bug fixes - Use volume storage instead of water storage (#102) ([`4cf20bf`](https://github.com/Bluetooth-Devices/bthome-ble/commit/4cf20bfd027286ba7ca842125d3cafbc0797e6c1)) ## v3.4.0 (2024-01-10) ### Features - Add water storage sensor (#101) ([`6eb80e0`](https://github.com/Bluetooth-Devices/bthome-ble/commit/6eb80e0e120f7c8ef6ce4b7ee5057ae5356192b3)) ## v3.3.1 (2023-12-15) ### Bug fixes - No check of encryption counter when bindkey has not been verified (#97) ([`43f1412`](https://github.com/Bluetooth-Devices/bthome-ble/commit/43f14123b68732f41552d2f198dbbf2323763614)) ## v3.3.0 (2023-12-13) ### Features - Add check for increasing encryption counter (#92) ([`a58cf7b`](https://github.com/Bluetooth-Devices/bthome-ble/commit/a58cf7bf84bacaf0b8436f03499710d5b83e3add)) ## v3.2.0 (2023-10-01) ### Features - Add raw hex sensor (#91) ([`6f9969e`](https://github.com/Bluetooth-Devices/bthome-ble/commit/6f9969e73efe0ccaa810d3e7aa136af673eca3c2)) ### Refactoring - Remove dependency on pytz (#90) ([`4619caa`](https://github.com/Bluetooth-Devices/bthome-ble/commit/4619caa03eab37c7e992b1cf8caaf253818ff816)) ## v3.1.1 (2023-08-20) ### Bug fixes - Replace datetime.utcfromtimestamp (#87) ([`0184fe7`](https://github.com/Bluetooth-Devices/bthome-ble/commit/0184fe7b04caabc45fe3e8da1191269a7d19b975)) ## v3.1.0 (2023-08-13) ### Features - Add text sensor (#86) ([`2bfcd9b`](https://github.com/Bluetooth-Devices/bthome-ble/commit/2bfcd9b9b4a2321eb229298258235b61a97ae9d2)) ## v3.0.0 (2023-07-15) ### Features - Add ability to set the bind key after init (#83) ([`1826139`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1826139cc81bbdb0b9fdd616b16fa92b07654f6b)) ## v2.13.0 (2023-07-15) ### Unknown ### Bug fixes - Lint ([`576342d`](https://github.com/Bluetooth-Devices/bthome-ble/commit/576342d480f7a2f01337e96141dc4db2d896a82e)) ### Features - Switch from pycryptodomex to cryptography ([`0cdc8e6`](https://github.com/Bluetooth-Devices/bthome-ble/commit/0cdc8e674bc5f6d6f8cef64891d30c1e90f415ae)) ## v2.12.2 (2023-07-15) ### Unknown ### Bug fixes - Decryption bug fixed ([`dfb12e6`](https://github.com/Bluetooth-Devices/bthome-ble/commit/dfb12e6d46e29cadaff39b5e9d6625a708502d05)) ## v2.12.1 (2023-07-07) ### Unknown ### Chores - Bump bluetooth-sensor-state-data ([`54556a3`](https://github.com/Bluetooth-Devices/bthome-ble/commit/54556a35529d37a07420623f6c4b4911c49a33d5)) ### Bug fixes - Bump sensor-state-data to 2.16.1 ([`9657a91`](https://github.com/Bluetooth-Devices/bthome-ble/commit/9657a91bd39550b74fe3d822c1e47b407aa25131)) - Fix for repeated events ([`285da3c`](https://github.com/Bluetooth-Devices/bthome-ble/commit/285da3c76e719043295225f81b9c96279ea7554c)) ## v2.12.0 (2023-06-18) ### Unknown ### Bug fixes - Bump semantic release ([`ebe3484`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ebe34848db5d3a88ac2023cb26bf2803e51b88fd)) - Fix failing tests ([`3cf0634`](https://github.com/Bluetooth-Devices/bthome-ble/commit/3cf0634cf2eb91b55990746f161d9605e00d0fde)) - Use utcfromtimestamp ([`1fea0eb`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1fea0eb3a257c5c4e8070fa40ee221a5a297ef86)) ### Features - Timestamp acceleration and gyroscope ([`07c6b40`](https://github.com/Bluetooth-Devices/bthome-ble/commit/07c6b4023dd3e960ab16bb80a5565f91738b55ad)) ## v2.11.3 (2023-05-19) ### Unknown ### Bug fixes - Revert trigger based device name ([`f2b5303`](https://github.com/Bluetooth-Devices/bthome-ble/commit/f2b5303952d5e4ec1ac63307ba43aca7c9262a36)) ## v2.11.2 (2023-05-12) ### Unknown ### Bug fixes - Pre-commit bump and bump release ([`97540cc`](https://github.com/Bluetooth-Devices/bthome-ble/commit/97540cccf52e6c49357082917228a1b704579ba0)) ## v2.11.1 (2023-05-12) ### Unknown ### Bug fixes - Add trigger based device to device type ([`0d0076b`](https://github.com/Bluetooth-Devices/bthome-ble/commit/0d0076ba6ee54fd1d68a67453c549f27fc9e5a2d)) ## v2.11.0 (2023-05-04) ### Unknown ### Bug fixes - Clean code ([`26bc5d2`](https://github.com/Bluetooth-Devices/bthome-ble/commit/26bc5d2ebd2e840c2a32b140275c8eee7c2f79e8)) ### Features - Add sleepy device bit ([`d61e63e`](https://github.com/Bluetooth-Devices/bthome-ble/commit/d61e63ebd68461fae26ef08b05c5d5771dae45fb)) ## v2.10.1 (2023-04-28) ### Unknown ### Bug fixes - Add sleepy sensors to init ([`7a82a50`](https://github.com/Bluetooth-Devices/bthome-ble/commit/7a82a50f2be23e3c429626fc98f03a6ab80ed7bb)) ## v2.10.0 (2023-04-28) ### Unknown ### Bug fixes - Typo ([`7ddb135`](https://github.com/Bluetooth-Devices/bthome-ble/commit/7ddb1357421e9f419529955e05cb98915539aeab)) ### Features - Add sleepy devices ([`2bcbf38`](https://github.com/Bluetooth-Devices/bthome-ble/commit/2bcbf38e51bb6d1a0791682acceacb721edfc866)) ### Chores - Add tests for shelly button ([`c8c8365`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c8c83654f7493e647fbb14d12d1715497dde51dc)) ## v2.9.0 (2023-03-12) ### Unknown ### Features - Add water sensor ([`c228d77`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c228d772a4c2757e59697b9b233057d1648f9525)) ## v2.8.0 (2023-03-05) ### Unknown ### Features - Add gas water energy meter ([`152bcea`](https://github.com/Bluetooth-Devices/bthome-ble/commit/152bcea19394fa32b82fca1d2cfffcba5609d81c)) ## v2.7.0 (2023-02-25) ### Unknown ### Features - Add gas sensor ([`956610f`](https://github.com/Bluetooth-Devices/bthome-ble/commit/956610fd72e5490f0dbf47ab5cbac7871c8d2565)) ## v2.6.0 (2023-02-17) ### Unknown ### Bug fixes - Fix flake 8 line length error ([`e7490b4`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e7490b487201abf74c6e7271cee1d9eab1db2c2c)) ### Features - Add possibility to include mac in payload ([`1059aa1`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1059aa1eca2fc7ac7518e0a9dbbb822548417bf0)) ## v2.5.2 (2023-02-10) ### Bug fixes - Parse objects of the same time correctly (#47) ([`27e5982`](https://github.com/Bluetooth-Devices/bthome-ble/commit/27e5982214ee34cca6243532dff4e9b2e65e0da9)) ## v2.5.1 (2023-01-24) ### Unknown ### Bug fixes - Correct data length check ([`aed08c4`](https://github.com/Bluetooth-Devices/bthome-ble/commit/aed08c4d07a7fc1dd25d5f081a88d88171946498)) ## v2.5.0 (2023-01-11) ### Unknown ### Features - Add voltage with 1 digit ([`acd1da7`](https://github.com/Bluetooth-Devices/bthome-ble/commit/acd1da7e62a652cba588d8b543479afd6b86a42f)) ## v2.4.1 (2023-01-04) ### Unknown ### Bug fixes - Bump sensor-state-data ([`1202ace`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1202ace26e0a5acdb26cc90e4eae1d8deab3a2dd)) - Bump sensor-state-data ([`7bff7be`](https://github.com/Bluetooth-Devices/bthome-ble/commit/7bff7be46ad1c0ca082c7801f7741343c92cb342)) - Bump sensor-state-data ([`930b222`](https://github.com/Bluetooth-Devices/bthome-ble/commit/930b22299e20c61bd8256b4a9db3b4306e4a224a)) ## v2.4.0 (2022-12-17) ### Unknown ### Bug fixes - Update tests and dependencies ([`bb9d0a2`](https://github.com/Bluetooth-Devices/bthome-ble/commit/bb9d0a2c4941915dd9e37b8fe8db3ef5f8c627d3)) ### Features - Add volume and packet id ([`aaefa4e`](https://github.com/Bluetooth-Devices/bthome-ble/commit/aaefa4e5913bc6db80ad98e4e2947fb58e8abb83)) ## v2.3.1 (2022-11-19) ### Unknown ### Bug fixes - Warning for numerical order object id ([`b86feaa`](https://github.com/Bluetooth-Devices/bthome-ble/commit/b86feaa7f2845c1471bd13d48443a23122078bc6)) ## v2.3.0 (2022-11-18) ### Unknown ### Features - Remove cipher update ([`21b9390`](https://github.com/Bluetooth-Devices/bthome-ble/commit/21b9390953341e540a42636f49b09ab17519ef5c)) ## v2.2.1 (2022-11-07) ### Unknown ### Bug fixes - Multiple measurements fix ([`849deab`](https://github.com/Bluetooth-Devices/bthome-ble/commit/849deab688a83ba040e8f7e42598b4aa339d0460)) ## v2.2.0 (2022-11-06) ### Unknown ### Bug fixes - Add tests for duration and temperature ([`552cbb8`](https://github.com/Bluetooth-Devices/bthome-ble/commit/552cbb8be03e62b1b9e2231ba4c658c658d375cd)) - Uv and duration sensors ([`4130c71`](https://github.com/Bluetooth-Devices/bthome-ble/commit/4130c71b01f4a1d733e11b153fbe50031b53b48b)) ### Features - Update dependencies ([`a3e9daf`](https://github.com/Bluetooth-Devices/bthome-ble/commit/a3e9dafcaa907ae3672c60df7e2da3ce8128b340)) ## v2.1.0 (2022-11-03) ### Unknown ### Bug fixes - Update poetry file ([`90cf1ea`](https://github.com/Bluetooth-Devices/bthome-ble/commit/90cf1eaf4e8b474dfda13c6f579a46a3f08e95d2)) ### Features - New sensor types ([`1bbf778`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1bbf778887cb411d710a7787c3ae0f68a9a9eac2)) ## v2.0.0 (2022-11-01) ### Features - V2 release bump ([`93bfd22`](https://github.com/Bluetooth-Devices/bthome-ble/commit/93bfd22eff117f1cf2b98248b2ccf9b9dd90e6d8)) - V2 release bump ([`3487df3`](https://github.com/Bluetooth-Devices/bthome-ble/commit/3487df36be7a478d5b5512f348ade6cb43a67079)) ### Unknown ## v1.4.0 (2022-11-01) ### Unknown ### Bug fixes - Parser not parsing multiple uuids (#21) ([`e8d2646`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e8d2646bb73d39e7920a04243d862c9ed68ed2c0)) - Adjust button events ([`b796cdb`](https://github.com/Bluetooth-Devices/bthome-ble/commit/b796cdb9d18703c12bfbc1a03602edae823df6f0)) - Logs and fix for wrong id ([`fc611c9`](https://github.com/Bluetooth-Devices/bthome-ble/commit/fc611c940295f7254bb89911d450e3abee544880)) - Fix v1 tests ([`8099cad`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8099cadcf47efdfa10a4945b8cad6cbd9fad88f6)) - Add annotations ([`fb19043`](https://github.com/Bluetooth-Devices/bthome-ble/commit/fb19043808124ba9cfcea340c6144c9d19abc84a)) - Button and dimmer events ([`ac3b8fa`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ac3b8faac93913cf93f2095053dee580407ae939)) - Wrong bthome device info byte ([`97d03ef`](https://github.com/Bluetooth-Devices/bthome-ble/commit/97d03efe657d875c06b6018453cc692a73a14531)) - Remove device_info_flag ([`f524294`](https://github.com/Bluetooth-Devices/bthome-ble/commit/f524294f29ad3df43066c5e13faf90c45e1e9b7d)) - Resolve comments ([`5a904f6`](https://github.com/Bluetooth-Devices/bthome-ble/commit/5a904f6c97c2ad17ad706c224dbc5c2b2265f7b3)) - Improve coverage ([`f21a739`](https://github.com/Bluetooth-Devices/bthome-ble/commit/f21a739325ed2fe20d22256fe6e085c6404f8b12)) - Resolve comments review ([`e0d58a0`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e0d58a0e139f1b7928ff3407a56c02e40df9c5da)) - Remove mac parser and code cleaning ([`4a4fe68`](https://github.com/Bluetooth-Devices/bthome-ble/commit/4a4fe683b781caf05524f6a38290d8215bb3a4b6)) ### Features - Remove object format and length byte ([`2f43a29`](https://github.com/Bluetooth-Devices/bthome-ble/commit/2f43a296cfb59fcb2938502801b3f4d12785358c)) - Remove predefined device info ([`cb16b35`](https://github.com/Bluetooth-Devices/bthome-ble/commit/cb16b35edec71c609ae12281f900f6f56d300e93)) - Multiple measurements of the same type ([`e57ed3d`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e57ed3d857164d373bba3ca86f368a9a4ed9030f)) - Uuid v2 and adv_info byte ([`a4d6440`](https://github.com/Bluetooth-Devices/bthome-ble/commit/a4d64403dd359102762f2dbea777ccdf5e58a21d)) ## v1.3.0 (2022-10-04) ### Features - Force new release adding events ([`8ae802a`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8ae802a6a34eb9419504347a5a498bf9617063d1)) ### Unknown ## v1.2.3 (2022-09-29) ### Unknown ### Bug fixes - Add missing comma ([`0f889e6`](https://github.com/Bluetooth-Devices/bthome-ble/commit/0f889e62b83247e7d0bb6f7edfeb695691a5f782)) - Remove unused imports ([`8f45606`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8f45606d106f7cd43a1875c8d36226ab73745ede)) - Add test for encryption example ([`c323a31`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c323a31355592947f4c00cb0b6061f175f3a0686)) - Fix formatting issues ([`47017b6`](https://github.com/Bluetooth-Devices/bthome-ble/commit/47017b6ed584a49a4ee2aa85ee678f3de2cd120b)) - Fix annotation ([`1bc8f72`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1bc8f72de8d807c333dc8ba767c5a0c2043dc4b1)) - Fix formatting issues ([`801679e`](https://github.com/Bluetooth-Devices/bthome-ble/commit/801679eda2524f01d48e76cab6bf5310ade682ec)) - Remove use of predefined sensor wrapper ([`377adaa`](https://github.com/Bluetooth-Devices/bthome-ble/commit/377adaa576b03a76340570729dfd4ac2bf019b36)) - Remove unused lists ([`fd08fae`](https://github.com/Bluetooth-Devices/bthome-ble/commit/fd08faee434bc3b66aec241627407edc3eab4d80)) ## v1.2.2 (2022-09-14) ### Unknown ### Bug fixes - Remove update binary sensor ([`e529f89`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e529f899eb7b9f38b65c36f5cc46d3df41e91f75)) ## v1.2.1 (2022-09-13) ### Unknown ### Bug fixes - Remove empty line ([`5f7ba24`](https://github.com/Bluetooth-Devices/bthome-ble/commit/5f7ba2438f4a8e6bde341b74668514906364d070)) - Sort import order ([`ba5afd1`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ba5afd11bea7678f0a9af50ad9c4a08a0800b62c)) - Always use bthome device classes ([`15fa937`](https://github.com/Bluetooth-Devices/bthome-ble/commit/15fa93718d7b506cc25e80831f7612423bd13306)) ## v1.2.0 (2022-09-09) ### Unknown ### Bug fixes - Update poetry lock file ([`477f974`](https://github.com/Bluetooth-Devices/bthome-ble/commit/477f974aba5ef9d469975473902994d01ed00181)) ## v1.1.1 (2022-09-08) ### Bug fixes - Flake8 error ([`4ed3312`](https://github.com/Bluetooth-Devices/bthome-ble/commit/4ed3312453c35d1911bb463a2c0b35310626e84f)) - Remove unused import ([`44f171a`](https://github.com/Bluetooth-Devices/bthome-ble/commit/44f171aad9375877dfba4f55d6f6f2ede12ccdad)) - Mypy error ([`716f626`](https://github.com/Bluetooth-Devices/bthome-ble/commit/716f626729080c451c24dd7da242d97709d620a7)) - Add binary sensor device class ([`d48783e`](https://github.com/Bluetooth-Devices/bthome-ble/commit/d48783e1a7c3a3a719fc7dca9f08feb034660507)) ### Features - Add binary sensors ([`c83d0da`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c83d0dab1aa44730976c0185c73d928a72a01b75)) ## v1.1.0 (2022-09-06) ### Unknown ### Bug fixes - Fix tests ([`ffce92d`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ffce92d599d1831ae241e10bddb874b5d5761a15)) - Fix flake8 tests ([`b428339`](https://github.com/Bluetooth-Devices/bthome-ble/commit/b428339e38f28caf17adc02b48a9585f05b629d1)) ### Features - Binary sensor support ([`35ec5e1`](https://github.com/Bluetooth-Devices/bthome-ble/commit/35ec5e1d328038b328c4b793e9fe70c354d731dd)) ## v1.0.0 (2022-09-01) ### Unknown ### Features - Change name of bthome with capital h ([`cf3c702`](https://github.com/Bluetooth-Devices/bthome-ble/commit/cf3c702ca4b2e25cc15a74379deeb84a949427d7)) - Bthome with capital h ([`c307462`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c3074628da771ce78810538e8776797aae4df147)) ## v0.5.2 (2022-08-30) ### Bug fixes - Use full name for bparasite ([`bcd07da`](https://github.com/Bluetooth-Devices/bthome-ble/commit/bcd07da262009383f4e67592fec6614937fe847f)) ## v0.5.1 (2022-08-29) ### Bug fixes - Fix tests for non standard device classes ([`d48ec31`](https://github.com/Bluetooth-Devices/bthome-ble/commit/d48ec31005c35d8f064bf2717c648b058bb1b468)) - Use update_sonsor for other device classes ([`eabde87`](https://github.com/Bluetooth-Devices/bthome-ble/commit/eabde8776ac6cdf9e7d1753aa4b2909d769de37d)) ## v0.5.0 (2022-08-28) ### Unknown ### Bug fixes - Add test for b-parasite ([`ecbb823`](https://github.com/Bluetooth-Devices/bthome-ble/commit/ecbb823ce604903a391df8c1e3a4f4d1558abc74)) - Bump sensor-state-data ([`e3e21af`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e3e21af1a7df36891aa504891afdab9caef82b06)) - Lint errors ([`8c3697e`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8c3697e479f20ce41763fbd32c9872af6974d990)) ### Features - Add new sensor types ([`15ac53a`](https://github.com/Bluetooth-Devices/bthome-ble/commit/15ac53af864ba3446973a4518f6dd30509bc1347)) ## v0.4.0 (2022-08-26) ### Features - Get manufacturer from name ([`1e66c90`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1e66c90cbe884852dab74c57708ba702ecafc736)) ## v0.3.8 (2022-08-25) ### Bug fixes - Failing reauth ([`7b3b0d2`](https://github.com/Bluetooth-Devices/bthome-ble/commit/7b3b0d2c66546c1b2d55d42ec95d94a0d7117ab5)) ## v0.3.7 (2022-08-25) ### Unknown ### Bug fixes - Code format ([`f5c531f`](https://github.com/Bluetooth-Devices/bthome-ble/commit/f5c531f73ea4be7dd749f0301bb1e486a0493106)) - Use short address from data tools ([`722a97f`](https://github.com/Bluetooth-Devices/bthome-ble/commit/722a97f513b7c6db63173433dea629ba59445fda)) ## v0.3.6 (2022-08-24) ### Bug fixes - Flake8 error ([`cc9f077`](https://github.com/Bluetooth-Devices/bthome-ble/commit/cc9f077ab9ce3685305a2407b17150a6e7f9bac5)) - Table format ([`eca5d42`](https://github.com/Bluetooth-Devices/bthome-ble/commit/eca5d42e710c5edadc190f289d88219f43699ee6)) - Units of voc ([`0f4c773`](https://github.com/Bluetooth-Devices/bthome-ble/commit/0f4c773c02310a3546c2687cf5d18d31677c0549)) ## v0.3.5 (2022-08-24) ### Bug fixes - Workaround for empty service_uuids ([`2cab081`](https://github.com/Bluetooth-Devices/bthome-ble/commit/2cab081c591de7807c02424819bfca243d360b69)) ## v0.3.4 (2022-08-23) ### Unknown ### Bug fixes - Length check ([`9817c84`](https://github.com/Bluetooth-Devices/bthome-ble/commit/9817c84f78993c743c354a09789354c6fa600a18)) ## v0.3.3 (2022-08-23) ### Bug fixes - Minor change to force a new release ([`a55133a`](https://github.com/Bluetooth-Devices/bthome-ble/commit/a55133a243c3b40b4a1a330a70f4aa8930d5dc8a)) ### Unknown ## v0.3.2 (2022-08-21) ### Unknown ## v0.3.1 (2022-08-21) ### Bug fixes - Supported sensor fix ([`0bc45bf`](https://github.com/Bluetooth-Devices/bthome-ble/commit/0bc45bf822da65d705823daff57881c917b9c8fc)) ### Unknown ## v0.3.0 (2022-08-19) ### Bug fixes - Remove double mac from name ([`eb77ad8`](https://github.com/Bluetooth-Devices/bthome-ble/commit/eb77ad8d947d53bfc585515cc512d088913f69e1)) - Isort ([`d85e345`](https://github.com/Bluetooth-Devices/bthome-ble/commit/d85e345ddfc4380c317df83774ef9fe61594d037)) ### Features - Add encryption support ([`e299228`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e299228e02313c154027bdef017a64db5d7fbe4c)) ## v0.2.2 (2022-08-18) ### Unknown ## v0.2.1 (2022-08-18) ### Bug fixes - Auto release test ([`8fa2ba6`](https://github.com/Bluetooth-Devices/bthome-ble/commit/8fa2ba6831e793652d0658292730942345585bf5)) - Auto release ([`1eac241`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1eac2415693e1a60d948e023fce5b8db108d56c1)) ## v0.2.0 (2022-08-18) ### Unknown ### Bug fixes - Delete poetry log ([`1376c47`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1376c473aa5f9c3783f9f2b03dfccf483f224335)) - Update sensor-state-data version ([`132cf9e`](https://github.com/Bluetooth-Devices/bthome-ble/commit/132cf9e74560a2abd5e263ddda04c70c9a6a1cfd)) ### Features - Update dependency ([`b624ef5`](https://github.com/Bluetooth-Devices/bthome-ble/commit/b624ef51b40f371eb23dfe2a66b2ecc1752832fd)) - Update dependencies ([`b0177f4`](https://github.com/Bluetooth-Devices/bthome-ble/commit/b0177f4427464e54262e1ca087d94bad570087a7)) ## v0.1.0 (2022-08-18) ### Bug fixes - Increase line length ([`1dd1baa`](https://github.com/Bluetooth-Devices/bthome-ble/commit/1dd1baa45549b2739ca0e7d787046cf2dfc637d4)) - Increase line length ([`c8d9a56`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c8d9a56a9d61390b9617b7ec0a8ea83d267615d6)) - Flake 8 errors ([`71fae3c`](https://github.com/Bluetooth-Devices/bthome-ble/commit/71fae3c1c008874e8c5af492b714c035c7e1a872)) - Flake8 errors ([`dd7a283`](https://github.com/Bluetooth-Devices/bthome-ble/commit/dd7a283582b48556a9212f3702d287111a3847b3)) - Flake8 errors ([`b36bb2d`](https://github.com/Bluetooth-Devices/bthome-ble/commit/b36bb2d4c79ba1823d290449a65965ee01145dee)) - Mypy error ([`1364971`](https://github.com/Bluetooth-Devices/bthome-ble/commit/13649712943bfacacf6e3e2e53a2ea677073962e)) - Formatting conflicts ([`e2548b0`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e2548b00485e3485ee91715b5229a60b4104aa01)) - Formatting errors ([`7bef4a3`](https://github.com/Bluetooth-Devices/bthome-ble/commit/7bef4a34beb38d94bc023aefc964a8945e43ff6b)) - Flake8 errors ([`8634293`](https://github.com/Bluetooth-Devices/bthome-ble/commit/863429339ad6e1f1b8a75b0bc06283979110783a)) - Bump sensor-state-data ([`d9e409d`](https://github.com/Bluetooth-Devices/bthome-ble/commit/d9e409dbb5612fba786338f35a4354d1e1a0463c)) - Linting errors ([`b1dc181`](https://github.com/Bluetooth-Devices/bthome-ble/commit/b1dc1815cf49100b94f99430948757f0790a5a04)) - Linting errors ([`d47bfc9`](https://github.com/Bluetooth-Devices/bthome-ble/commit/d47bfc99223a7bb5237db7ced36b3e73992b599e)) - Linting errors ([`e33c68f`](https://github.com/Bluetooth-Devices/bthome-ble/commit/e33c68f7698a3ff5ee1ac9bc3bcb408d9458aae5)) ### Features - Initial release ([`c097555`](https://github.com/Bluetooth-Devices/bthome-ble/commit/c0975559955bddab4c96aa993e381107b4f7c152)) ### Unknown bthome-ble-3.12.5/CONTRIBUTING.md000066400000000000000000000103701477343332500160650ustar00rootroot00000000000000# 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] for code issues or feature requests. 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. Documentation issues of the website [bthome.io](https://bthome.io) can be reported at the [issue page of the website][doc-issues]. ### 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 BTHome BLE could always use more documentation, whether as part of the official BTHome BLE docs, in docstrings, or even on the web in blog posts, articles, and such. The official website is located at [bthome.io](https://bthome.io). Changes to the website can be done on the [GitHub page of the site](https://github.com/home-assistant/bthome.io). ### 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/bthome-ble.git ``` 3. Go to the cloned folder: ```shell $ cd bthome-ble ``` 4. Install the project dependencies with [Poetry](https://python-poetry.org): ```shell $ poetry install ``` 5. Create a branch for local development: ```shell $ git checkout -b name-of-your-bugfix-or-feature ``` Now you can make your changes locally. 6. When you're done making changes, check that your changes pass our tests: ```shell $ poetry run pytest ``` 7. 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 ``` 8. 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. 9. 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/bthome-ble/issues [doc-issues]: https://github.com/home-assistant/bthome.io/issues bthome-ble-3.12.5/LICENSE000066400000000000000000000020561477343332500146430ustar00rootroot00000000000000 MIT License Copyright (c) 2022 Ernst Klamer 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. bthome-ble-3.12.5/README.md000066400000000000000000000073661477343332500151260ustar00rootroot00000000000000# BTHome BLE

CI Status Documentation Status Test coverage percentage

Poetry black pre-commit

PyPI Version Supported Python versions License

BLE parser for sensors that support the BTHome BLE format (V1 and V2). ## Installation Install this via pip (or your favourite package manager): `pip install bthome-ble` ## BThome BLE format More detailed information about the BTHome BLE format can be found on the [usage page](https://bthome.io/) ## 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. [![An open standard from the Open Home Foundation](https://www.openhomefoundation.org/badges/ohf-open-standard.png)](https://www.openhomefoundation.org/) bthome-ble-3.12.5/commitlint.config.mjs000066400000000000000000000003621477343332500177720ustar00rootroot00000000000000export 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], }, }; bthome-ble-3.12.5/docs/000077500000000000000000000000001477343332500145635ustar00rootroot00000000000000bthome-ble-3.12.5/docs/Makefile000066400000000000000000000011751477343332500162270ustar00rootroot00000000000000# 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) bthome-ble-3.12.5/docs/make.bat000066400000000000000000000013741477343332500161750ustar00rootroot00000000000000@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 bthome-ble-3.12.5/docs/source/000077500000000000000000000000001477343332500160635ustar00rootroot00000000000000bthome-ble-3.12.5/docs/source/_static/000077500000000000000000000000001477343332500175115ustar00rootroot00000000000000bthome-ble-3.12.5/docs/source/_static/.gitkeep000066400000000000000000000000001477343332500211300ustar00rootroot00000000000000bthome-ble-3.12.5/docs/source/changelog.md000066400000000000000000000000451477343332500203330ustar00rootroot00000000000000```{include} ../../CHANGELOG.md ``` bthome-ble-3.12.5/docs/source/conf.py000066400000000000000000000036321477343332500173660ustar00rootroot00000000000000# 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 = "BTHome BLE" copyright = "2022, E. Klamer" author = "E. Klamer" # -- 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"] bthome-ble-3.12.5/docs/source/contributing.md000066400000000000000000000000501477343332500211070ustar00rootroot00000000000000```{include} ../../CONTRIBUTING.md ``` bthome-ble-3.12.5/docs/source/index.md000066400000000000000000000003521477343332500175140ustar00rootroot00000000000000# Welcome to BTHome BLE documentation! ```{toctree} :caption: Installation & Usage :maxdepth: 2 installation usage ``` ```{toctree} :caption: Project Info :maxdepth: 2 changelog contributing ``` ```{include} ../../README.md ``` bthome-ble-3.12.5/docs/source/installation.md000066400000000000000000000002621477343332500211060ustar00rootroot00000000000000# Installation The package is published on [PyPI](https://pypi.org/project/bthome-ble/) and can be installed with `pip` (or any equivalent): ```bash pip install bthome-ble ``` bthome-ble-3.12.5/docs/source/usage.md000066400000000000000000000002031477343332500175040ustar00rootroot00000000000000# Usage BTHome for DIY sensors Detailed information about the use of BTHome BLE can be found on our [website](https://bthome.io). bthome-ble-3.12.5/poetry.lock000066400000000000000000004402341477343332500160360ustar00rootroot00000000000000# 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"] 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 = "alabaster" version = "0.7.16" description = "A light, configurable Sphinx theme" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] name = "async-interrupt" version = "1.2.2" description = "Context manager to raise an exception when a future is done" optional = false python-versions = ">=3.9" groups = ["main"] files = [ {file = "async_interrupt-1.2.2-py3-none-any.whl", hash = "sha256:0a8deb884acfb5fe55188a693ae8a4381bbbd2cb6e670dac83869489513eec2c"}, {file = "async_interrupt-1.2.2.tar.gz", hash = "sha256:be4331a029b8625777905376a6dc1370984c8c810f30b79703f3ee039d262bf7"}, ] [[package]] name = "babel" version = "2.17.0" description = "Internationalization utilities" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "bleak" version = "0.22.3" description = "Bluetooth Low Energy platform Agnostic Klient" optional = false python-versions = "<3.14,>=3.8" groups = ["main"] 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] 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-retry-connector" version = "3.10.0" description = "A connector for Bleak Clients that handles transient connection failures" optional = false python-versions = ">=3.10" groups = ["main"] files = [ {file = "bleak_retry_connector-3.10.0-py3-none-any.whl", hash = "sha256:caaf976320ef280f1145b557bf3b13697f71ef2c1070e1dc643709eb2d29fb1f"}, {file = "bleak_retry_connector-3.10.0.tar.gz", hash = "sha256:a95172bd56d2af677fb9e250291cde8c70d8f72381d423f64e48c828dffbc93b"}, ] [package.dependencies] bleak = {version = ">=0.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.14\""} bluetooth-adapters = {version = ">=0.15.2", markers = "python_version >= \"3.10\" and python_version < \"3.14\" and platform_system == \"Linux\""} dbus-fast = {version = ">=1.14.0", markers = "platform_system == \"Linux\""} [[package]] name = "bleak-winrt" version = "1.2.0" description = "Python WinRT bindings for Bleak" optional = false python-versions = "*" groups = ["main"] markers = "platform_system == \"Windows\" and python_version < \"3.12\"" files = [ {file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"}, {file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"}, {file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"}, {file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"}, {file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"}, {file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"}, {file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"}, {file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"}, {file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"}, {file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"}, {file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"}, ] [[package]] name = "bluetooth-adapters" version = "0.21.4" description = "Tools to enumerate and find Bluetooth Adapters" optional = false python-versions = ">=3.9" groups = ["main"] 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" 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 = "bluetooth-auto-recovery" version = "1.4.5" description = "Recover bluetooth adapters that are in an stuck state" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] files = [ {file = "bluetooth_auto_recovery-1.4.5-py3-none-any.whl", hash = "sha256:a55667366cbc29808877092ecd98e4ffc87957fb5012755904f766f2a42f52f0"}, {file = "bluetooth_auto_recovery-1.4.5.tar.gz", hash = "sha256:1c7c231bb53262bea8d15e72601ea0c839c3c6e5f840cd1c752e5c137b23aa17"}, ] [package.dependencies] bluetooth-adapters = ">=0.16.0" btsocket = ">=0.2.0" PyRIC = ">=0.1.6.3" usb-devices = ">=0.4.1" [package.extras] docs = ["Sphinx (>=5,<8)", "myst-parser (>=0.18,<3.1)", "sphinx-rtd-theme (>=1,<4)"] [[package]] name = "bluetooth-data-tools" version = "1.26.5" description = "Tools for converting bluetooth data and packets" optional = false python-versions = ">=3.10" groups = ["main"] files = [ {file = "bluetooth_data_tools-1.26.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2275525cff8004d00b0e06f72570b81792a2aedb5fed77e7d04bf866b271a64"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aaf952e52c4d21617a32c2c180e0499c44ffde1528997207c679968ea0141f20"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a3612085eec298acf56c7a90885aa1242dab311dd52827a2a120f5ec6816d9b"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfe1206c8a51382a3d756fad04f64f62902a1b28c11d2eedc62b12454c14029d"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0deabddf73d8dd79dba717f7974365a9de377c29b64b405c794b106a3ebc98d3"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef2a13aa88c91824d0d679a26b8c4684e3d107f2f8e8dab056f4c3d2a8f16157"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:20caea0fca7d6adf1c04a2e9622bfd05da2ef3492755142d1ff494174ff31198"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f53d0def7b753c576d75e56da30067873e4beba460c8221c0850099df56a3bae"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2d142f2e6f64ce42e05c944c93342b5f5cd7f72d00c9eeca12fe3fdec157af63"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:33ccfe730c47a9c641c20afaa2b4308099a9b8847c61800b12b3db92cf55ca00"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-win32.whl", hash = "sha256:1518ceb70cbc9ac094ea86efe47b014f83433ba193505f45f2b9780e57460a80"}, {file = "bluetooth_data_tools-1.26.5-cp310-cp310-win_amd64.whl", hash = "sha256:04f03ef7b1226d747033eef868de222bcc62b6544fa59f997a30cc13b4ebcc0c"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c1def549ad3805c2af610b585762da6f5fa72c123851b9895586c0bd5759ce0"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ec3e8c43efe1fa2e649f042b6b9b834b9711b64020e340d885a99d41fd43652"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5ed47efccb952775218dbf67468b4b210892a17a278a4786248c1d36647eb8f"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55cf273a7f07627b0a38860328f513e3aabfe33b5b80ce232d3a9740522d06ac"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eff2600404e3e65b8bfc24aca0338bb93f195027c7906f74c75c0c5bda3c265"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8466e989ccd6cbcec184da95635ab0295eff88494cc3e89ae691885a3e5984eb"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:26669a95d76ec4ac8512fbd0064218e4fcf74c96f93a0eecf9612ece966efed1"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a65809ddc64f60678817bf09715bfd209cdc431a29ccf10460de14ced43f2e9a"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a010c0563ce297d7cd5fa07bfd00865acecd29458f84f1c3fe00d1526ba3183e"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:312dd1178c007333246b066eeec5a17f4d433d18095eed52aadbaf26cfd67e6d"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-win32.whl", hash = "sha256:62cbb6656f845e14757b7f2b688916b6a045e63036121c97d6326f3f9b73f915"}, {file = "bluetooth_data_tools-1.26.5-cp311-cp311-win_amd64.whl", hash = "sha256:58e95dd8809681a2ec63d0b671c2528c9d97e7e502f65c669881bb1c9a661089"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7a2e4c8c6b5a0f3c711a96688ea5f6316e5ab4fe62bde24cdc94c9610ab9c3e"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c446465593f4b591b25bb7a45d31c64850a9e997b81ca03f2c91be5130c407"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b41f9efd2d949c5236065992e5bd1dc2540c1d0465784859976e345e0547e0"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e4936e139be55f9d79b2d33570230767d9dd5bf28e5224c172b5605118d7860"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7531a41499f62ef3d92a614d8b405f150d38c1051beb06cc23f330eaf4e579a"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:744df8bfa5e88354ac4a509208fb48b22efdaa05ae2c4be2d2cbd96cd6be9924"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:649e2a85322efacab90abb1ac4c3636803e1dbef66d2af57ac062192d38ed30c"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:668c51ce51769c9240ce9191d91776d25ceb45469ad76576e6ca6ffb1575ece4"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:15b66c1e293a31029e403d848daf171d854cd597e03694aafb4cc8454fe74658"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8df78a2c855cd6b0241afcbec16984ff9e027be13f53fed72889c06bd9b4c8d"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-win32.whl", hash = "sha256:0af9086037099fe4dec1d41c3fc040d38812046551d4fb1fbfe492772ff68d20"}, {file = "bluetooth_data_tools-1.26.5-cp312-cp312-win_amd64.whl", hash = "sha256:1b122108d9ad99c7bf11f1cf2c266524cf6550ac3e9e4dcbd8cf0f95a61c3aa6"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2641cbafe5098ab4c8ca498a7d5f099276c2241a7e856e16945511217d0e465a"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c514fb4910ffdf932eb22cf2e69fa532c04af0605e26f826ff81949dfe501fe"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85cd3a2d888b34668765214c3f8b38b0a81b76046966d86a42cc3f75318c177f"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6031cabe4bf6c46cff724b52c3ec529d5495f778d6605938fca8fc88d4685000"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4827d00f97bed3fdf58c5ad54eecdc696807d0d1cc9afc1450afd87ff4d1dc"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ec0d9adf9f26b5b473324fdd07784802c083d5db55c1a5b0dbd6af0425a96b9"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:cefb6a74e3a1ec713e3e04689203a3d19d0daf485e0d1d847bf168eeae3642bb"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df2f2eb5655b54a08a50db5a3934845af843f023b4f44e49098785e24030bf06"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ccb2ec0b6e0f595386229192874220bad885727a05928f55ab0b6cd8daa9a8d"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:01117b5e8cccda48a3945810f04f30749788bed318087296de2b02ebb887f395"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5da4569305350478e3fb043ebfdd1a2de6ffe75d310638bbc4b42ec37214aec4"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-win32.whl", hash = "sha256:fd335f4dd8486ff5f12f1abe1f6ce8090d86a2052cb7beab0456606c0b9420f0"}, {file = "bluetooth_data_tools-1.26.5-cp313-cp313-win_amd64.whl", hash = "sha256:bad3557e8dcdd617828bcc898ef5c858d70a8b2206e2a264c6d33d477d203123"}, {file = "bluetooth_data_tools-1.26.5.tar.gz", hash = "sha256:6c89a91ccc1280b7337d7a8d5dac3e8b03240b0687256d615302d26eb155bda3"}, ] [package.dependencies] cryptography = ">=41.0.3" [package.extras] docs = ["Sphinx (>=5,<9)", "myst-parser (>=0.18,<4.1)", "sphinx-rtd-theme (>=1,<4)"] [[package]] name = "bluetooth-sensor-state-data" version = "1.7.5" description = "Models for storing and converting Bluetooth Sensor State Data" optional = false python-versions = ">=3.9" groups = ["main"] files = [ {file = "bluetooth_sensor_state_data-1.7.5-py3-none-any.whl", hash = "sha256:a4ce3cce9839422299209e5427a6ce24061ff844fbdb61af2a485e480b8d7e23"}, {file = "bluetooth_sensor_state_data-1.7.5.tar.gz", hash = "sha256:be9319a3d70745e11689e91c013bf71d0d5570d303a49d21668777ea7854227a"}, ] [package.dependencies] home-assistant-bluetooth = ">=1.3.0" sensor-state-data = ">=2.0" [package.extras] docs = ["Sphinx (>=5,<7)", "myst-parser (>=0.18,<3.1)", "sphinx-rtd-theme (>=1,<4)"] [[package]] name = "btsocket" version = "0.3.0" description = "Python library for BlueZ Bluetooth Management API" optional = false python-versions = "*" groups = ["main"] files = [ {file = "btsocket-0.3.0-py2.py3-none-any.whl", hash = "sha256:949821c1b580a88e73804ad610f5173d6ae258e7b4e389da4f94d614344f1a9c"}, {file = "btsocket-0.3.0.tar.gz", hash = "sha256:7ea495de0ff883f0d9f8eea59c72ca7fed492994df668fe476b84d814a147a0d"}, ] [package.extras] dev = ["bumpversion", "coverage", "pycodestyle", "pygments", "sphinx", "sphinx-rtd-theme", "twine"] docs = ["pygments", "sphinx", "sphinx-rtd-theme"] rel = ["bumpversion", "twine"] test = ["coverage", "pycodestyle"] [[package]] name = "certifi" version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = true python-versions = ">=3.6" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] name = "cffi" version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" groups = ["main"] markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] pycparser = "*" [[package]] name = "charset-normalizer" version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = true python-versions = ">=3.7" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[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 = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] markers = {main = "extra == \"docs\" and sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} [[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.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" version = "44.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main"] files = [ {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"}, {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"}, {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"}, {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"}, {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"}, {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"}, {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"}, {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"}, {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"}, {file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"}, {file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"}, {file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"}, {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"}, {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"}, {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"}, {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"}, {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"}, {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"}, {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"}, {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"}, {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"}, {file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"}, {file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"}, {file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"}, {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"}, {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"}, {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"}, {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"}, {file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"}, {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"}, {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"}, {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"}, {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"}, {file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] name = "dbus-fast" version = "2.44.0" description = "A faster version of dbus-next" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Linux\"" files = [ {file = "dbus_fast-2.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7e3eff5c37c782bcc5b73556ae0726cb1391bd5673b877c301761e8bfc93b21a"}, {file = "dbus_fast-2.44.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a052ee5c01d2bdc7083df941ffbb424bd76350e4755aa996d345aa5b21bbe54"}, {file = "dbus_fast-2.44.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:d8cf14a46cb8ebe51ff1d8c0be293c5656a60cb4321162277dd32ee683fc6cce"}, {file = "dbus_fast-2.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d891a46610f54e43a847fe2f670e9f6f68277bc495b1565801002f6ab6f0854"}, {file = "dbus_fast-2.44.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4977cad841b89f2654363f56ac9e13431bcae75e201ff4a42d8840f665da3ab3"}, {file = "dbus_fast-2.44.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2dcdd81d6c4a5cb165518cd50cc0827ad6c145b712ce9c4da618b427f87abe3f"}, {file = "dbus_fast-2.44.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7382fc12b6f84b47f47ccf2242f16f10ac2dc6a8966eea9944324b839fd7f2cc"}, {file = "dbus_fast-2.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9940b5d1cf4ada5278b601cd6335d66f66017be6dfb5104d704d6e80728265fe"}, {file = "dbus_fast-2.44.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639b0ceb3a6d8d5e72119e92ffc9e9b9308dc81c9ad64fc7db76fa5e5f08bedc"}, {file = "dbus_fast-2.44.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:e6221b137df24e79ddb422dcf2c77623f0ea0a75f3b940dc2c2b84bae73f869d"}, {file = "dbus_fast-2.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4de0dbc141bc1c10f0721f0600d3b7bdee8b050a7dbb9b1adbcca7aa8ea18dab"}, {file = "dbus_fast-2.44.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1397d6e8026e0ffad4e622c90e200df92d0e3b1b9c6f4646bf29ea58c3bb4f8b"}, {file = "dbus_fast-2.44.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:abde213a27491f1d7262148d05efcc137a70a4b0cf038b7ca6b0cda22905ce1c"}, {file = "dbus_fast-2.44.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f854a86f37a06e6d6f6f12c2401159919ea92e66736c91a96c4a5e3bfc7dcde4"}, {file = "dbus_fast-2.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dfc604949d822f5746ea5e699dea33c31b8407131fd98d7c5aa04b1ecc541a51"}, {file = "dbus_fast-2.44.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a68915536a4a37be5460a4c4837f0c2f909983d34fc7f8af84abdfa03cdf8f48"}, {file = "dbus_fast-2.44.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4402e7f3d86bc751399ca09e8c143aa2bfce2ec27b76305e311a28f29b307c96"}, {file = "dbus_fast-2.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67061253a5f57526492adac595f6c7f8196a5bee13aa247cba335206358b3ffe"}, {file = "dbus_fast-2.44.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2cfcd0701f39a9789f8b61e2fc7d14c28c01b7045b0fc4dff88ae564aa912746"}, {file = "dbus_fast-2.44.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e3bc861080e8359bee1c7b7dc2e00106a7df5e9906a937fab960e97ddb864091"}, {file = "dbus_fast-2.44.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:734799964722c309f52a1aaf5f0bfab19ba5cc44c9704d0a3a5bc0084ea7c81e"}, {file = "dbus_fast-2.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ec3ef966b5bd48678130462e9814abb4eedba2394bdc1821022ff69a50653999"}, {file = "dbus_fast-2.44.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a728f59c9752f3f77668878df7068283726fa003c63661d8c809ca0a6e5e0a"}, {file = "dbus_fast-2.44.0-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:7b2b559eaf350cdadf6e33018121d2b0e80cc66047942505f19c4d1453cf8b26"}, {file = "dbus_fast-2.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721d7a5698c489fc2c0456fa80fd76d664e2808bd9683e3061749aee63925040"}, {file = "dbus_fast-2.44.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:045aa9c18e2449b7c3e84fa4caccdd1f9020d8a3d22f0dfcc11083ed4ccf2884"}, {file = "dbus_fast-2.44.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fa27ef923f96418a1dcc331df3be972716cc7a70799e1d2be6ba52534300006"}, {file = "dbus_fast-2.44.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:405a9219e9b9d5b204f61a484c291855cc4d17cae14dcca2224bb3483b34dabb"}, {file = "dbus_fast-2.44.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420bb5a3ba2be81a3710159c85577fde176c117ffc4c3b6c87ca10babb1c4"}, {file = "dbus_fast-2.44.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:36d32327e3e3716b03a80044d661515a39d313a9fdd125ed278e9a213de2d24d"}, {file = "dbus_fast-2.44.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c33d71b9a8e68bd9b483efcb4bdeb0466b6347d2bbaeaf6c24e6d4b432e3c5f"}, {file = "dbus_fast-2.44.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4684c18f4c6563359b01bc0abc906c37a11019f87451a94584e32ad706f0acd8"}, {file = "dbus_fast-2.44.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48736fdc97be846aaecc2e23f8a2dcdaced22e6a9eff1c71184e007d671e0348"}, {file = "dbus_fast-2.44.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6aae77badf1a3a69a54e2b8fe81bd027b6ce002c174d67a3354b3361a4b17096"}, {file = "dbus_fast-2.44.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:98d4fffc492964a204fa5117e1f89363c4d029c00b3232df24583cbd47adaeaf"}, {file = "dbus_fast-2.44.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e7dec20398dfe0506b0e68810b87854695e00dc6552cefe32d8459f23c76d4e5"}, {file = "dbus_fast-2.44.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:955c5a8ad3152331310f8dabf1809f940f46adc50ab0102a9865b087113e0e37"}, {file = "dbus_fast-2.44.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59ca2a83ce8394c2f0a4105fa462d18dc937869dd73051a8ad5a1371314f0f94"}, {file = "dbus_fast-2.44.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:3a97d5f509cc6667e1884d5a6c2d7a3b5d62befde41fbf21741a043a526fd013"}, {file = "dbus_fast-2.44.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f4e6bda1302d1404b83043a6bdef0ee1800fc8926dbdc4fd5e6dd6085874af1"}, {file = "dbus_fast-2.44.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b431ca0904624fc98a74410547036108aa71ffdffd2d2704bbd8883a3d9541c"}, {file = "dbus_fast-2.44.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbe729bcf9b4942cfe99d53377a5c0043530ed98a3affe5245d76b19eb5c435d"}, {file = "dbus_fast-2.44.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:5e28b72452ad6f3cfbd8104d0ac4ab2d36a2a6aeea8c88a6bf1ec864c5e15a93"}, {file = "dbus_fast-2.44.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:475a1c5228e85695609724f57457d35d88e57f3b70c1f9bcfee26ec8821da110"}, {file = "dbus_fast-2.44.0.tar.gz", hash = "sha256:ccdf9a77447ce6cefaa869431fca2512e75ab4a4549c4b35a093988949e35f43"}, ] [[package]] name = "docutils" version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] name = "habluetooth" version = "3.38.0" description = "High availability Bluetooth" optional = false python-versions = ">=3.11" groups = ["main"] files = [ {file = "habluetooth-3.38.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1881c146d5b84c5a1f07fe0113281166d35be19ab1cc08ce3f9ace19af165ef3"}, {file = "habluetooth-3.38.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07561287ea6afab873678c60b5d592b515add1ddee0d9a7564f97e55e3778d50"}, {file = "habluetooth-3.38.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1909b53d5906fca6a6f88e08f27430d565f5abacdcd983882133e4fca2840e"}, {file = "habluetooth-3.38.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9d406429a193ace0d8d5ab0e6d322aa51b793455f2d03a26ba738ee47b710c55"}, {file = "habluetooth-3.38.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2401d83a0c6dee999d514722b9373a733525abec148a90be6a20bd0e56cbfaf8"}, {file = "habluetooth-3.38.0-cp311-cp311-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:810dcdf4cf2cad298d9200c5aaa0332b42719230de91d810ce82f98aed58ceea"}, {file = "habluetooth-3.38.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ec7afd0ea2a5af6cccb87834836dfcf55354c80e13613d3ae29e126123f09ac"}, {file = "habluetooth-3.38.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3490ccc601dfb717318b2d1251a289e8578bc9e42df4534e57e7c2d08c3947e5"}, {file = "habluetooth-3.38.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9376f819578edcf44a4d352c9ace7e7f42ab7560b13024442dd07f2abb887016"}, {file = "habluetooth-3.38.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b169edba40de9d4b29178320409da9137c2d348b7e89df7eae4f37aff32b92e7"}, {file = "habluetooth-3.38.0-cp311-cp311-win32.whl", hash = "sha256:b852744e6303f25eeec4673d4e9ab62e9abfa2e945d35388aa28b45eb9702b10"}, {file = "habluetooth-3.38.0-cp311-cp311-win_amd64.whl", hash = "sha256:a38824ed6e92409ace2de3e345f5e78b38bc853a5ee5af986ba30ed80441adc2"}, {file = "habluetooth-3.38.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1545bfd35a1286dff9737cd0273b2db0d2cdaa2794d3e13de39822204fd6783f"}, {file = "habluetooth-3.38.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:338405eff410edfd2053014191a9389401e7d8db3da74f962a83d07ce0202617"}, {file = "habluetooth-3.38.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01ac6c678a596771ec2a626ca0e1bbc84f1b92d449a56dbdbd1c5dfd1affba6"}, {file = "habluetooth-3.38.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:6b34afda7868e9af8150271de129a66d4fdeadbf654c8c99ab8465ed574e436f"}, {file = "habluetooth-3.38.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2c122801406546764e23e26d211940cfaeec5abddbc7276b4bad264211797b2"}, {file = "habluetooth-3.38.0-cp312-cp312-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f18ed561187e1ad4789364cb45d8de196b8b7701e45a2817db85ef304dcbb2d6"}, {file = "habluetooth-3.38.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d08b029a99eb97025eb79c6c91202315d48e72fe65b2066cfe4079ece0527248"}, {file = "habluetooth-3.38.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:710da6a6493cfeb154b70d6316abe5b200ad030cd458c71ffd58fba02e5117b1"}, {file = "habluetooth-3.38.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c2fdfcb075ef1b548f1af1b13a471c7d3a782696d645147571196062599e77c"}, {file = "habluetooth-3.38.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e1061f6bdce4d80e884065dbf0a963c2dd3311d5c28d5c30f3ae99c126c2aa1e"}, {file = "habluetooth-3.38.0-cp312-cp312-win32.whl", hash = "sha256:cdf4c70adb903f233735bb64f3fffe00809e209489178ac26065eb403e0cf299"}, {file = "habluetooth-3.38.0-cp312-cp312-win_amd64.whl", hash = "sha256:e1d8feb52a26ab5746422ff7474afd20f0908676ec5d3c117a4e000a1a6f624e"}, {file = "habluetooth-3.38.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2fad8f120aa7ea60bb0cc5813e9bca7cf71aa73bad0c4963329ce6061a586a3a"}, {file = "habluetooth-3.38.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be3d47624e24f041f51f2f27a7c12e4c668d10babe4e5e405fa95e99cd7643b4"}, {file = "habluetooth-3.38.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75bab83183932cf72ba1022a59f89a7ab38ff1eb89976a3ad69632f2152b31ab"}, {file = "habluetooth-3.38.0-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:7bb74e4806282560fd59d069824af5665656daed64bc7b732387d523f44b0c4e"}, {file = "habluetooth-3.38.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b87c68c439999b1f47a686334633c75d5885cecb6863ef824331d1ba29c684c"}, {file = "habluetooth-3.38.0-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:677137a00cbb1fad58f9201fdad16b6d16b9fc9f11556db2ed6fe3013e717656"}, {file = "habluetooth-3.38.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:6a382b10467262326f3944c0822d61d3ebef8f4587fd256cf9567f8b553225f5"}, {file = "habluetooth-3.38.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8c53ccec8a747d5488f52eb7b415482d584a1025b4c228f410b04fd8da535db7"}, {file = "habluetooth-3.38.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a8919c1b84969ea906e586e68c2e36880be652263a16f343ccaf16ba6b47d4d6"}, {file = "habluetooth-3.38.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3f9ad0b37c3b05c67621e7e0321d40628f1955c3ada0dbdb46180d0b60e1855e"}, {file = "habluetooth-3.38.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2c1aad5ad0e04504a710b3d75be1ee49d9629d42dc7e454830699245c1872a1f"}, {file = "habluetooth-3.38.0-cp313-cp313-win32.whl", hash = "sha256:4f4adfd4ea2acb669f6cfef7dbccb4ea4ed0892850c0d312de9197f3dc559109"}, {file = "habluetooth-3.38.0-cp313-cp313-win_amd64.whl", hash = "sha256:4881c9e0c497441a8ac392c9b6236ae1ac01af94ffbcb038436b8157ec43a571"}, {file = "habluetooth-3.38.0.tar.gz", hash = "sha256:1a191bf48373edfb8e476acd6a79f756a1de3aafc78210a96271e9e32f6b5131"}, ] [package.dependencies] async-interrupt = ">=1.1.1" bleak = ">=0.21.1" bleak-retry-connector = ">=3.9.0" bluetooth-adapters = ">=0.16.1" bluetooth-auto-recovery = ">=1.2.3" bluetooth-data-tools = ">=1.16.0" [[package]] name = "home-assistant-bluetooth" version = "1.13.1" description = "Home Assistant Bluetooth Models and Helpers" optional = false python-versions = ">=3.11" groups = ["main"] files = [ {file = "home_assistant_bluetooth-1.13.1-py3-none-any.whl", hash = "sha256:cdf13b5b45f7744165677831e309ee78fbaf0c2866c6b5931e14d1e4e7dae5d7"}, {file = "home_assistant_bluetooth-1.13.1.tar.gz", hash = "sha256:0ae0e2a8491cc762ee9e694b8bc7665f1e2b4618926f63969a23a2e3a48ce55e"}, ] [package.dependencies] habluetooth = ">=3.0" [[package]] name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = true python-versions = ">=3.6" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] [[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 = "jinja2" version = "3.1.6" description = "A very fast and expressive template engine." optional = true python-versions = ">=3.7" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] code-style = ["pre-commit (>=3.0,<4.0)"] compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] name = "mdit-py-plugins" version = "0.4.2" description = "Collection of plugins for markdown-it-py" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, ] [package.dependencies] markdown-it-py = ">=1.0.0,<4.0.0" [package.extras] code-style = ["pre-commit"] rtd = ["myst-parser", "sphinx-book-theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" optional = true python-versions = ">=3.7" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] name = "myst-parser" version = "4.0.1" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d"}, {file = "myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4"}, ] [package.dependencies] docutils = ">=0.19,<0.22" jinja2 = "*" markdown-it-py = ">=3.0,<4.0" mdit-py-plugins = ">=0.4.1,<1.0" pyyaml = "*" sphinx = ">=7,<9" [package.extras] code-style = ["pre-commit (>=4.0,<5.0)"] linkify = ["linkify-it-py (>=2.0,<3.0)"] rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pygments (<2.19)", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] [[package]] name = "packaging" version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] markers = {main = "extra == \"docs\""} [[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 = "pycparser" version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["main"] markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] name = "pygments" version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyobjc-core" version = "10.3.2" description = "Python<->ObjC Interoperability Module" optional = false python-versions = ">=3.8" groups = ["main"] markers = "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 = "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 = "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 = "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 = "pyric" version = "0.1.6.3" description = "Python Wireless Library" optional = false python-versions = "*" groups = ["main"] files = [ {file = "PyRIC-0.1.6.3.tar.gz", hash = "sha256:b539b01cafebd2406c00097f94525ea0f8ecd1dd92f7731f43eac0ef16c2ccc9"}, ] [[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\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[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 = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] name = "requests" version = "2.32.3" description = "Python HTTP for Humans." optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "sensor-state-data" version = "2.18.1" description = "Models for storing and converting Sensor Data state" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] files = [ {file = "sensor_state_data-2.18.1-py3-none-any.whl", hash = "sha256:45a223acf5d4aefde45c028fa810c7925db6448984097aa1b500fe4f206d113f"}, {file = "sensor_state_data-2.18.1.tar.gz", hash = "sha256:25f17ed98748ae006ddab82d5013cf30301daaf23526d1992f99c4dc0beb49c3"}, ] [package.extras] docs = ["Sphinx (>=5.0,<6.0)", "myst-parser (>=0.18,<0.19)", "sphinx-rtd-theme (>=1.0,<2.0)"] [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = true python-versions = "*" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] [[package]] name = "sphinx" version = "7.4.7" description = "Python documentation generator" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, ] [package.dependencies] alabaster = ">=0.7.14,<0.8.0" babel = ">=2.13" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} docutils = ">=0.20,<0.22" imagesize = ">=1.3" Jinja2 = ">=3.1" packaging = ">=23.0" Pygments = ">=2.17" requests = ">=2.30.0" snowballstemmer = ">=2.2" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.9" [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] name = "sphinx-rtd-theme" version = "3.0.2" description = "Read the Docs theme for Sphinx" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, ] [package.dependencies] docutils = ">0.18,<0.22" sphinx = ">=6,<9" sphinxcontrib-jquery = ">=4,<5" [package.extras] dev = ["bump2version", "transifex-client", "twine", "wheel"] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jquery" version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = true python-versions = ">=2.7" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, ] [package.dependencies] Sphinx = ">=1.8" [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = true python-versions = ">=3.5" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] [package.extras] test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["defusedxml (>=0.7.1)", "pytest"] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[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"] 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 = "urllib3" version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "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"] 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 = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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 = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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 = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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 = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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 = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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 = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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 = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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 = "platform_system == \"Windows\" and python_version >= \"3.12\"" 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)"] [extras] docs = ["Sphinx", "myst-parser", "sphinx-rtd-theme"] [metadata] lock-version = "2.1" python-versions = "^3.11,<3.14" content-hash = "31bbe727ca86be894bc25d542e097866e1cb273b866b3e4159385ea1e1fdf2fc" bthome-ble-3.12.5/pyproject.toml000066400000000000000000000047631477343332500165610ustar00rootroot00000000000000[project] name = "bthome-ble" version = "3.12.5" description = "BThome BLE support" authors = [{ name = "Ernst Klamer", email = "e.klamer@gmail.com" }] license = "MIT" readme = "README.md" requires-python = ">=3.11" dynamic = ["classifiers", "dependencies", "optional-dependencies"] [project.urls] "Repository" = "https://github.com/bluetooth-devices/bthome-ble" "Documentation" = "https://bthome-ble.readthedocs.io" "Bug Tracker" = "https://github.com/bluetooth-devices/bthome-ble/issues" "Changelog" = "https://github.com/bluetooth-devices/bthome-ble/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 = "bthome_ble", from = "src" }, ] [tool.poetry.dependencies] python = "^3.11,<3.14" # Documentation Dependencies Sphinx = {version = ">=5,<8", optional = true} sphinx-rtd-theme = {version = ">=1,<4", optional = true} myst-parser = {version = ">=0.18,<4.1", optional = true} bluetooth-sensor-state-data = ">=1.6.1" sensor-state-data = ">=2.16.1" bluetooth-data-tools = ">=0.1.2" cryptography = ">=40.0.0" habluetooth = ">=3.0" [tool.poetry.extras] docs = [ "myst-parser", "sphinx", "sphinx-rtd-theme", ] [tool.poetry.group.dev.dependencies] pytest = "^8.3" pytest-cov = "^6.0" [tool.semantic_release] branch = "main" version_toml = ["pyproject.toml:project.version"] version_variable = "src/bthome_ble/__init__.py:__version__" build_command = "pip install poetry && poetry build" [tool.pytest.ini_options] addopts = "-v -Wdefault --cov=bthome_ble --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 = ["bthome_ble", "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" bthome-ble-3.12.5/renovate.json000066400000000000000000000001011477343332500163410ustar00rootroot00000000000000{ "extends": ["github>browniebroke/renovate-configs:python"] } bthome-ble-3.12.5/setup.py000066400000000000000000000003601477343332500153440ustar00rootroot00000000000000#!/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="bthome-ble") bthome-ble-3.12.5/src/000077500000000000000000000000001477343332500144225ustar00rootroot00000000000000bthome-ble-3.12.5/src/bthome_ble/000077500000000000000000000000001477343332500165225ustar00rootroot00000000000000bthome-ble-3.12.5/src/bthome_ble/__init__.py000066400000000000000000000011431477343332500206320ustar00rootroot00000000000000"""Parser for BLE advertisements in BTHome format.""" from __future__ import annotations from sensor_state_data import ( BinarySensorDeviceClass, DeviceClass, DeviceKey, SensorDescription, SensorDeviceClass, SensorDeviceInfo, SensorUpdate, SensorValue, Units, ) from .parser import BTHomeBluetoothDeviceData __version__ = "3.9.2" __all__ = [ "BinarySensorDeviceClass", "BTHomeBluetoothDeviceData", "DeviceClass", "DeviceKey", "SensorDescription", "SensorDeviceClass", "SensorDeviceInfo", "SensorUpdate", "SensorValue", "Units", ] bthome-ble-3.12.5/src/bthome_ble/bthome_v1_encryption.py000066400000000000000000000070251477343332500232360ustar00rootroot00000000000000"""Example showing encoding and decoding of BTHome v1 advertisement""" from __future__ import annotations import binascii from cryptography.hazmat.primitives.ciphers.aead import AESCCM def parse_value(data: bytes) -> dict[str, float]: """Parse decrypted payload to readable BTHome data""" vlength = len(data) if vlength >= 3: temp = round(int.from_bytes(data[2:4], "little", signed=False) * 0.01, 2) humi = round(int.from_bytes(data[6:8], "little", signed=False) * 0.01, 2) print("Temperature:", temp, "Humidity:", humi) return {"temperature": temp, "humidity": humi} print("MsgLength:", vlength, "HexValue:", data.hex()) return {} def decrypt_payload( payload: bytes, mic: bytes, key: bytes, nonce: bytes ) -> dict[str, float] | None: """Decrypt payload.""" print("Nonce:", nonce.hex()) print("CryptData:", payload.hex()) print("Mic:", mic.hex()) cipher = AESCCM(key, tag_length=4) print() print("Starting Decryption data") try: data = cipher.decrypt(nonce, payload + mic, b"\x11") except ValueError as error: print() print("Decryption failed:", error) return None print("Decryption succeeded, decrypted data:", data.hex()) print() return parse_value(data=data) def decrypt_aes_ccm(key: bytes, mac: bytes, data: bytes) -> dict[str, float] | None: """Decrypt AES CCM.""" print("MAC:", mac.hex()) print("Bindkey:", key.hex()) adslength = len(data) if adslength > 15 and data[0] == 0x1E and data[1] == 0x18: pkt = data[: data[0] + 1] uuid = pkt[0:2] encrypted_data = pkt[2:-8] count_id = pkt[-8:-4] mic = pkt[-4:] # nonce: mac [6], uuid16 [2], count_id [4] # 6+2+4 = 12 bytes nonce = b"".join([mac, uuid, count_id]) return decrypt_payload(encrypted_data, mic, key, nonce) else: print("Error: format packet!") return None def encrypt_payload( data: bytes, mac: bytes, uuid16: bytes, count_id: bytes, key: bytes ) -> bytes: """Encrypt payload.""" nonce = b"".join([mac, uuid16, count_id]) # 6+2+4 = 12 bytes cipher = AESCCM(key, tag_length=4) associated_data = b"\x11" result = cipher.encrypt(nonce, data, associated_data) ciphertext = result[:-4] mic = result[-4:] print("MAC:", mac.hex()) print("Binkey:", key.hex()) print("Data:", data.hex()) print("Nonce:", nonce.hex()) print("CryptData:", ciphertext.hex(), "Mic:", mic.hex()) return b"".join([uuid16, ciphertext, count_id, mic]) # ============================= # main() # ============================= def main() -> None: """Example to encrypt and decrypt BTHome payload.""" print() print("====== Test encode -----------------------------------------") data = bytes(bytearray.fromhex("2302CA090303BF13")) # BTHome data (not encrypted) parse_value(data) # Print temperature and humidity print() print("Preparing data for encryption") count_id = bytes(bytearray.fromhex("00112233")) # count id (change every message) mac = binascii.unhexlify("5448E68F80A5") # MAC uuid16 = b"\x1e\x18" bindkey = binascii.unhexlify("231d39c1d7cc1ab1aee224cd096db932") payload = encrypt_payload( data=data, mac=mac, uuid16=uuid16, count_id=count_id, key=bindkey ) print() print("Encrypted data:", payload.hex()) print() print("====== Test decode -----------------------------------------") decrypt_aes_ccm(key=bindkey, mac=mac, data=payload) if __name__ == "__main__": main() bthome-ble-3.12.5/src/bthome_ble/bthome_v2_encryption.py000066400000000000000000000072661477343332500232460ustar00rootroot00000000000000"""Example showing encoding and decoding of BTHome v2 advertisement""" from __future__ import annotations import binascii from cryptography.hazmat.primitives.ciphers.aead import AESCCM def parse_value(data: bytes) -> dict[str, float]: """Parse decrypted payload to readable BTHome data""" vlength = len(data) if vlength >= 3: temp = round(int.from_bytes(data[1:3], "little", signed=False) * 0.01, 2) humi = round(int.from_bytes(data[4:6], "little", signed=False) * 0.01, 2) print("Temperature:", temp, "Humidity:", humi) return {"temperature": temp, "humidity": humi} print("MsgLength:", vlength, "HexValue:", data.hex()) return {} def decrypt_payload( payload: bytes, mic: bytes, key: bytes, nonce: bytes ) -> dict[str, float] | None: """Decrypt payload.""" print("Nonce:", nonce.hex()) print("Ciphertext:", payload.hex()) print("MIC:", mic.hex()) cipher = AESCCM(key, tag_length=4) print() print("Starting Decryption data") try: data = cipher.decrypt(nonce, payload + mic, None) except ValueError as error: print() print("Decryption failed:", error) return None print("Decryption succeeded, decrypted data:", data.hex()) print() return parse_value(data=data) def decrypt_aes_ccm(key: bytes, mac: bytes, data: bytes) -> dict[str, float] | None: """Decrypt AES CCM.""" print("MAC:", mac.hex()) print("Bindkey:", key.hex()) adslength = len(data) if adslength > 15 and data[0] == 0xD2 and data[1] == 0xFC: pkt = data[: data[0] + 1] uuid = pkt[0:2] sw_version = pkt[2:3] encrypted_data = pkt[3:-8] count_id = pkt[-8:-4] mic = pkt[-4:] # nonce: mac [6], uuid16 [2], count_id [4] # 6+3+4 = 13 bytes nonce = b"".join([mac, uuid, sw_version, count_id]) return decrypt_payload(encrypted_data, mic, key, nonce) else: print("Error: format packet!") return None def encrypt_payload( data: bytes, mac: bytes, uuid16: bytes, sw_version: bytes, count_id: bytes, key: bytes, ) -> bytes: """Encrypt payload.""" nonce = b"".join([mac, uuid16, sw_version, count_id]) # 6+2+1+4 = 13 bytes cipher = AESCCM(key, tag_length=4) result = cipher.encrypt(nonce, data, None) ciphertext = result[:-4] mic = result[-4:] print("MAC:", mac.hex()) print("Binkey:", key.hex()) print("Data:", data.hex()) print("Nonce:", nonce.hex()) print("Ciphertext:", ciphertext.hex()) print("MIC:", mic.hex()) return b"".join([uuid16, sw_version, ciphertext, count_id, mic]) # ============================= # main() # ============================= def main() -> None: """Example to encrypt and decrypt BTHome payload.""" print() print("====== Test encode -----------------------------------------") data = bytes(bytearray.fromhex("02CA0903BF13")) # BTHome data (not encrypted) parse_value(data) # Print temperature and humidity print() print("Preparing data for encryption") count_id = bytes(bytearray.fromhex("00112233")) # count id (change every message) mac = binascii.unhexlify("5448E68F80A5") # MAC uuid16 = b"\xd2\xfc" sw_version = b"\x41" bindkey = binascii.unhexlify("231d39c1d7cc1ab1aee224cd096db932") payload = encrypt_payload( data=data, mac=mac, uuid16=uuid16, sw_version=sw_version, count_id=count_id, key=bindkey, ) print() print("Encrypted data:", payload.hex()) print() print("====== Test decode -----------------------------------------") decrypt_aes_ccm(key=bindkey, mac=mac, data=payload) if __name__ == "__main__": main() bthome-ble-3.12.5/src/bthome_ble/const.py000066400000000000000000000331571477343332500202330ustar00rootroot00000000000000"""Constants for BTHome measurements.""" import dataclasses from sensor_state_data import ( BaseDeviceClass, BinarySensorDeviceClass, SensorLibrary, Units, description, ) from .event import EventDeviceKeys @dataclasses.dataclass class MeasTypes: meas_format: ( EventDeviceKeys | description.BaseBinarySensorDescription | description.BaseSensorDescription ) data_length: int = 1 data_format: str = "unsigned_integer" factor: float = 1 class ExtendedSensorDeviceClass(BaseDeviceClass): """Device class for additional sensors (compared to sensor-state-data).""" # Data channel CHANNEL = "channel" # Raw hex data RAW = "raw" # Text TEXT = "text" # Volume storage VOLUME_STORAGE = "volume_storage" # Direction DIRECTION = "direction" # Precipitation PRECIPITATION = "precipitation" class ExtendedSensorLibrary(SensorLibrary): """Sensor Library for additional sensors (compared to sensor-state-data).""" RAW__NONE = description.BaseSensorDescription( device_class=ExtendedSensorDeviceClass.RAW, native_unit_of_measurement=None, ) TEXT__NONE = description.BaseSensorDescription( device_class=ExtendedSensorDeviceClass.TEXT, native_unit_of_measurement=None, ) VOLUME_STORAGE__VOLUME_LITERS = description.BaseSensorDescription( device_class=ExtendedSensorDeviceClass.VOLUME_STORAGE, native_unit_of_measurement=Units.VOLUME_LITERS, ) DIRECTION__DEGREE = description.BaseSensorDescription( device_class=ExtendedSensorDeviceClass.DIRECTION, native_unit_of_measurement=Units.DEGREE, ) PRECIPITATION__LENGTH_MILLIMETERS = description.BaseSensorDescription( device_class=ExtendedSensorDeviceClass.PRECIPITATION, native_unit_of_measurement=Units.LENGTH_MILLIMETERS, ) CHANNEL__NONE = description.BaseSensorDescription( device_class=ExtendedSensorDeviceClass.CHANNEL, native_unit_of_measurement=None, ) MEAS_TYPES: dict[int, MeasTypes] = { 0x00: MeasTypes(meas_format=SensorLibrary.PACKET_ID__NONE), 0x01: MeasTypes(meas_format=SensorLibrary.BATTERY__PERCENTAGE), 0x02: MeasTypes( meas_format=SensorLibrary.TEMPERATURE__CELSIUS, data_length=2, data_format="signed_integer", factor=0.01, ), 0x03: MeasTypes( meas_format=SensorLibrary.HUMIDITY__PERCENTAGE, data_length=2, factor=0.01, ), 0x04: MeasTypes( meas_format=SensorLibrary.PRESSURE__MBAR, data_length=3, factor=0.01, ), 0x05: MeasTypes( meas_format=SensorLibrary.LIGHT__LIGHT_LUX, data_length=3, factor=0.01, ), 0x06: MeasTypes( meas_format=SensorLibrary.MASS__MASS_KILOGRAMS, data_length=2, factor=0.01, ), 0x07: MeasTypes( meas_format=SensorLibrary.MASS__MASS_POUNDS, data_length=2, factor=0.01, ), 0x08: MeasTypes( meas_format=SensorLibrary.DEW_POINT__TEMP_CELSIUS, data_length=2, data_format="signed_integer", factor=0.01, ), 0x09: MeasTypes( meas_format=SensorLibrary.COUNT__NONE, data_length=1, ), 0x0A: MeasTypes( meas_format=SensorLibrary.ENERGY__ENERGY_KILO_WATT_HOUR, data_length=3, factor=0.001, ), 0x0B: MeasTypes( meas_format=SensorLibrary.POWER__POWER_WATT, data_length=3, factor=0.01, ), 0x0C: MeasTypes( meas_format=SensorLibrary.VOLTAGE__ELECTRIC_POTENTIAL_VOLT, data_length=2, factor=0.001, ), 0x0D: MeasTypes( meas_format=SensorLibrary.PM25__CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, data_length=2, ), 0x0E: MeasTypes( meas_format=SensorLibrary.PM10__CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, data_length=2, ), 0x0F: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.GENERIC, ), ), 0x10: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.POWER, ), ), 0x11: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.OPENING, ), ), 0x12: MeasTypes( meas_format=SensorLibrary.CO2__CONCENTRATION_PARTS_PER_MILLION, data_length=2, ), 0x13: MeasTypes( meas_format=( SensorLibrary.VOLATILE_ORGANIC_COMPOUNDS__CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ), data_length=2, ), 0x14: MeasTypes( meas_format=SensorLibrary.MOISTURE__PERCENTAGE, data_length=2, factor=0.01, ), 0x15: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.BATTERY, ) ), 0x16: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.BATTERY_CHARGING, ) ), 0x17: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.CO, ) ), 0x18: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.COLD, ) ), 0x19: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.CONNECTIVITY, ) ), 0x1A: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.DOOR, ) ), 0x1B: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.GARAGE_DOOR, ) ), 0x1C: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.GAS, ) ), 0x1D: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.HEAT, ) ), 0x1E: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.LIGHT, ) ), 0x1F: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.LOCK, ) ), 0x20: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.MOISTURE, ) ), 0x21: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.MOTION, ) ), 0x22: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.MOVING, ) ), 0x23: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.OCCUPANCY, ) ), 0x24: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.PLUG, ) ), 0x25: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.PRESENCE, ) ), 0x26: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.PROBLEM, ) ), 0x27: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.RUNNING, ) ), 0x28: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.SAFETY, ) ), 0x29: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.SMOKE, ) ), 0x2A: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.SOUND, ) ), 0x2B: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.TAMPER, ) ), 0x2C: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.VIBRATION, ) ), 0x2D: MeasTypes( meas_format=description.BaseBinarySensorDescription( device_class=BinarySensorDeviceClass.WINDOW, ) ), 0x2E: MeasTypes(meas_format=SensorLibrary.HUMIDITY__PERCENTAGE), 0x2F: MeasTypes(meas_format=SensorLibrary.MOISTURE__PERCENTAGE), 0x3A: MeasTypes(meas_format=EventDeviceKeys.BUTTON), 0x3C: MeasTypes( meas_format=EventDeviceKeys.DIMMER, data_length=2, ), 0x3D: MeasTypes( meas_format=SensorLibrary.COUNT__NONE, data_length=2, ), 0x3E: MeasTypes( meas_format=SensorLibrary.COUNT__NONE, data_length=4, ), 0x3F: MeasTypes( meas_format=SensorLibrary.ROTATION__DEGREE, data_length=2, data_format="signed_integer", factor=0.1, ), 0x40: MeasTypes( meas_format=SensorLibrary.DISTANCE__LENGTH_MILLIMETERS, data_length=2, factor=1, ), 0x41: MeasTypes( meas_format=SensorLibrary.DISTANCE__LENGTH_METERS, data_length=2, factor=0.1, ), 0x42: MeasTypes( meas_format=SensorLibrary.DURATION__TIME_SECONDS, data_length=3, factor=0.001, ), 0x43: MeasTypes( meas_format=SensorLibrary.CURRENT__ELECTRIC_CURRENT_AMPERE, data_length=2, factor=0.001, ), 0x44: MeasTypes( meas_format=SensorLibrary.SPEED__SPEED_METERS_PER_SECOND, data_length=2, factor=0.01, ), 0x45: MeasTypes( meas_format=SensorLibrary.TEMPERATURE__CELSIUS, data_length=2, data_format="signed_integer", factor=0.1, ), 0x46: MeasTypes( meas_format=SensorLibrary.UV_INDEX__NONE, data_length=1, factor=0.1, ), 0x47: MeasTypes( meas_format=SensorLibrary.VOLUME__VOLUME_LITERS, data_length=2, factor=0.1, ), 0x48: MeasTypes( meas_format=SensorLibrary.VOLUME__VOLUME_MILLILITERS, data_length=2, ), 0x49: MeasTypes( meas_format=SensorLibrary.VOLUME_FLOW_RATE__VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, data_length=2, factor=0.001, ), 0x4A: MeasTypes( meas_format=SensorLibrary.VOLTAGE__ELECTRIC_POTENTIAL_VOLT, data_length=2, factor=0.1, ), 0x4B: MeasTypes( meas_format=SensorLibrary.GAS__VOLUME_CUBIC_METERS, data_length=3, factor=0.001, ), 0x4C: MeasTypes( meas_format=SensorLibrary.GAS__VOLUME_CUBIC_METERS, data_length=4, factor=0.001, ), 0x4D: MeasTypes( meas_format=SensorLibrary.ENERGY__ENERGY_KILO_WATT_HOUR, data_length=4, factor=0.001, ), 0x4E: MeasTypes( meas_format=SensorLibrary.VOLUME__VOLUME_LITERS, data_length=4, factor=0.001, ), 0x4F: MeasTypes( meas_format=SensorLibrary.WATER__VOLUME_LITERS, data_length=4, factor=0.001, ), 0x50: MeasTypes( meas_format=SensorLibrary.TIMESTAMP__NONE, data_length=4, data_format="timestamp", ), 0x51: MeasTypes( meas_format=SensorLibrary.ACCELERATION__ACCELERATION_METERS_PER_SQUARE_SECOND, data_length=2, factor=0.001, ), 0x52: MeasTypes( meas_format=SensorLibrary.GYROSCOPE__GYROSCOPE_DEGREES_PER_SECOND, data_length=2, factor=0.001, ), 0x53: MeasTypes( meas_format=ExtendedSensorLibrary.TEXT__NONE, data_format="string", ), 0x54: MeasTypes( meas_format=ExtendedSensorLibrary.RAW__NONE, data_format="raw", ), 0x55: MeasTypes( meas_format=ExtendedSensorLibrary.VOLUME_STORAGE__VOLUME_LITERS, data_length=4, factor=0.001, ), 0x56: MeasTypes( meas_format=SensorLibrary.CONDUCTIVITY__CONDUCTIVITY, data_length=2, ), 0x57: MeasTypes( meas_format=SensorLibrary.TEMPERATURE__CELSIUS, data_length=1, data_format="signed_integer", ), 0x58: MeasTypes( meas_format=SensorLibrary.TEMPERATURE__CELSIUS, data_length=1, data_format="signed_integer", factor=0.35, ), 0x59: MeasTypes( meas_format=SensorLibrary.COUNT__NONE, data_length=1, data_format="signed_integer", ), 0x5A: MeasTypes( meas_format=SensorLibrary.COUNT__NONE, data_length=2, data_format="signed_integer", ), 0x5B: MeasTypes( meas_format=SensorLibrary.COUNT__NONE, data_length=4, data_format="signed_integer", ), 0x5C: MeasTypes( meas_format=SensorLibrary.POWER__POWER_WATT, data_length=4, factor=0.01, data_format="signed_integer", ), 0x5D: MeasTypes( meas_format=SensorLibrary.CURRENT__ELECTRIC_CURRENT_AMPERE, data_length=2, factor=0.001, data_format="signed_integer", ), 0x5E: MeasTypes( meas_format=ExtendedSensorLibrary.DIRECTION__DEGREE, data_length=2, factor=0.01, ), 0x5F: MeasTypes( meas_format=ExtendedSensorLibrary.PRECIPITATION__LENGTH_MILLIMETERS, data_length=2, factor=1, ), 0x60: MeasTypes(meas_format=ExtendedSensorLibrary.CHANNEL__NONE), } bthome-ble-3.12.5/src/bthome_ble/event.py000066400000000000000000000012421477343332500202140ustar00rootroot00000000000000"""Event constants for BTHome measurements.""" from __future__ import annotations from sensor_state_data.enum import StrEnum class EventDeviceKeys(StrEnum): """Keys for devices that send events.""" # Rocker switch SWITCH = "switch" # Dimmer DIMMER = "dimmer" # Button BUTTON = "button" BUTTON_EVENTS: dict[int, str | None] = { 0x00: None, 0x01: "press", 0x02: "double_press", 0x03: "triple_press", 0x04: "long_press", 0x05: "long_double_press", 0x06: "long_triple_press", 0x80: "hold_press", } DIMMER_EVENTS: dict[int, str | None] = { 0x00: None, 0x01: "rotate_left", 0x02: "rotate_right", } bthome-ble-3.12.5/src/bthome_ble/parser.py000066400000000000000000000646571477343332500204120ustar00rootroot00000000000000"""Parser for BLE advertisements in BTHome format. This file is shamelessly copied from the following repository: https://github.com/Ernst79/bleparser/blob/ac8757ad64f1fc17674dcd22111e547cdf2f205b/package/bleparser/ha_ble.py BTHome was originally developed as HA BLE for ble_monitor and has been renamed to BTHome for the Home Assistant Bluetooth integration. MIT License applies. """ from __future__ import annotations import logging import struct from datetime import datetime, timezone from enum import Enum from typing import Any from bluetooth_data_tools import short_address from bluetooth_sensor_state_data import BluetoothData from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import AESCCM from home_assistant_bluetooth import BluetoothServiceInfoBleak from sensor_state_data.description import ( BaseBinarySensorDescription, BaseSensorDescription, ) from .const import MEAS_TYPES from .event import BUTTON_EVENTS, DIMMER_EVENTS, EventDeviceKeys _LOGGER = logging.getLogger(__name__) class EncryptionScheme(Enum): # No encryption is needed to use this device NONE = "none" # 16 byte encryption key expected BTHOME_BINDKEY = "bthome_bindkey" def to_mac(addr: bytes) -> str: """Return formatted MAC address.""" return ":".join(f"{i:02X}" for i in addr) def parse_uint(data_obj: bytes, factor: float = 1.0) -> float: """Convert bytes (as unsigned integer) and factor to float.""" decimal_places = -int(f"{factor:e}".split("e")[-1]) return round( int.from_bytes(data_obj, "little", signed=False) * factor, decimal_places ) def parse_int(data_obj: bytes, factor: float = 1.0) -> float: """Convert bytes (as signed integer) and factor to float.""" decimal_places = -int(f"{factor:e}".split("e")[-1]) return round( int.from_bytes(data_obj, "little", signed=True) * factor, decimal_places ) def parse_float(data_obj: bytes, factor: float = 1.0) -> float | None: """Convert bytes (as float) and factor to float.""" decimal_places = -int(f"{factor:e}".split("e")[-1]) if len(data_obj) == 2: [val] = struct.unpack(" str | None: """Convert bytes to raw hex string.""" return data_obj.hex() def parse_string(data_obj: bytes) -> str | None: """Convert bytes to string.""" try: return data_obj.decode("UTF-8") except UnicodeDecodeError: _LOGGER.error( "BTHome data contains bytes that can't be decoded to a string (use UTF-8 encoding)" ) return None def parse_timestamp(data_obj: bytes) -> datetime: """Convert bytes to a datetime object.""" value = datetime.fromtimestamp( int.from_bytes(data_obj, "little", signed=False), tz=timezone.utc ) _LOGGER.error("time %s", value) return value def parse_event_type(event_device: str, data_obj: int) -> str | None: """Convert bytes to event type.""" if event_device == "dimmer": event_type = DIMMER_EVENTS.get(data_obj) elif event_device == "button": event_type = BUTTON_EVENTS.get(data_obj) else: event_type = None return event_type def parse_event_properties( event_device: str, data_obj: bytes ) -> dict[str, str | int | float | None] | None: """Convert bytes to event properties.""" if event_device == "dimmer": # number of steps for rotating a dimmer return {"steps": int.from_bytes(data_obj, "little", signed=True)} else: return None class BTHomeBluetoothDeviceData(BluetoothData): """Data for BTHome Bluetooth devices.""" def __init__(self, bindkey: bytes | None = None) -> None: super().__init__() self.set_bindkey(bindkey) # Data that we know how to parse but don't yet map to the SensorData model. self.unhandled: dict[str, Any] = {} # Encryption to expect, based on flags in the UUID. self.encryption_scheme = EncryptionScheme.NONE # The encryption counter can be used to verify that the counter of encrypted # advertisements is increasing, to have some replay protection. We always # start at zero allow the first message after a restart. self.encryption_counter = 0.0 # The packet_id is used to filter duplicate messages in BTHome V2. self.packet_id: float | None = None # If True then we have used the provided encryption key to decrypt at least # one payload. # If False then we have either not seen an encrypted payload, the key is wrong # or encryption is not in use self.bindkey_verified = False # If True then the decryption has failed or has not been verified yet. # If False then the decryption has succeeded. self.decryption_failed = True # If this is True, then we have not seen an advertisement with a payload # Until we see a payload, we can't tell if this device is encrypted or not self.pending = True # The last service_info we saw that had a payload # We keep this to help in reauth flows where we want to reprocess and old # value with a new bindkey. self.last_service_info: BluetoothServiceInfoBleak | None = None # If this is True, the device is not sending advertisements in a regular interval self.sleepy_device = False def set_bindkey(self, bindkey: bytes | None) -> None: """Set the bindkey.""" self.bindkey = bindkey if bindkey: self.cipher: AESCCM | None = AESCCM(bindkey, tag_length=4) else: self.cipher = None def supported(self, data: BluetoothServiceInfoBleak) -> bool: if not super().supported(data): return False return True def _start_update(self, service_info: BluetoothServiceInfoBleak) -> None: """Update from BLE advertisement data.""" _LOGGER.debug("Parsing BTHome BLE advertisement data: %s", service_info) for uuid, service_data in service_info.service_data.items(): if uuid in [ "0000181c-0000-1000-8000-00805f9b34fb", "0000181e-0000-1000-8000-00805f9b34fb", ]: if self._parse_bthome_v1(service_info, service_data): self.last_service_info = service_info elif uuid == "0000fcd2-0000-1000-8000-00805f9b34fb": if self._parse_bthome_v2(service_info, service_data): self.last_service_info = service_info return None def _parse_bthome_v1( self, service_info: BluetoothServiceInfoBleak, service_data: bytes ) -> bool: """Parser for BTHome sensors version V1""" identifier = short_address(service_info.address) name = service_info.name sw_version = 1 # Remove identifier from ATC sensors. atc_identifier = ( service_info.address.replace("-", "").replace(":", "")[-6:].upper() ) if name[-6:] == atc_identifier: name = name[:-6].rstrip(" _") # Try to get manufacturer if name.startswith(("ATC", "LYWSD03MMC")): manufacturer = "Xiaomi" elif name.startswith("prst"): manufacturer = "b-parasite" name = "b-parasite" else: manufacturer = None if manufacturer: self.set_device_manufacturer(manufacturer) self.set_device_name(f"{name} {identifier}") self.set_title(f"{name} {identifier}") self.set_device_type("BTHome sensor") uuid16 = list(service_info.service_data.keys()) if "0000181c-0000-1000-8000-00805f9b34fb" in uuid16: # Non-encrypted BTHome BLE format self.encryption_scheme = EncryptionScheme.NONE self.set_device_sw_version("BTHome BLE v1") payload = service_data elif "0000181e-0000-1000-8000-00805f9b34fb" in uuid16: # Encrypted BTHome BLE format self.encryption_scheme = EncryptionScheme.BTHOME_BINDKEY self.set_device_sw_version("BTHome BLE v1 (encrypted)") mac_readable = service_info.address source_mac = bytes.fromhex(mac_readable.replace(":", "")) try: payload = self._decrypt_bthome( service_info, service_data, source_mac, sw_version ) except (ValueError, TypeError): return True else: return False return self._parse_payload(payload, sw_version, service_info.time) def _parse_bthome_v2( self, service_info: BluetoothServiceInfoBleak, service_data: bytes ) -> bool: """Parser for BTHome sensors version V2""" identifier = short_address(service_info.address) name = service_info.name if name == service_info.address: name = "BTHome sensor" # Remove identifier from ATC sensors name. atc_identifier = ( service_info.address.replace("-", "").replace(":", "")[-6:].upper() ) if name[-6:] == atc_identifier: name = name[:-6].rstrip(" _") adv_info = service_data[0] # Determine if encryption is used encryption = adv_info & (1 << 0) # bit 0 if encryption == 1: self.encryption_scheme = EncryptionScheme.BTHOME_BINDKEY else: self.encryption_scheme = EncryptionScheme.NONE # If True, the first 6 bytes contain the mac address mac_included = adv_info & (1 << 1) # bit 1 if mac_included: bthome_mac_reversed = service_data[1:7] mac_readable = to_mac(bthome_mac_reversed[::-1]) payload = service_data[7:] else: mac_readable = service_info.address payload = service_data[1:] # If True, the device is only updating when triggered self.sleepy_device = bool(adv_info & (1 << 2)) # bit 2 # Check BTHome version sw_version = (adv_info >> 5) & 7 # 3 bits (5-7) if sw_version == 2: if self.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY: self.set_device_sw_version(f"BTHome BLE v{sw_version} (encrypted)") else: self.set_device_sw_version(f"BTHome BLE v{sw_version}") else: _LOGGER.error( "%s: Sensor is set to use BTHome version %s, which is not existing. " "Please modify the version in the first byte of the service data", identifier, sw_version, ) return False # Try to get manufacturer based on the name if name.startswith(("ATC", "LYWSD03MMC")): manufacturer = "Xiaomi" device_type = "Temperature/Humidity sensor" elif name.startswith("prst"): manufacturer = "b-parasite" name = "b-parasite" device_type = "Plant sensor" elif name.startswith("SBBT"): manufacturer = "Shelly" name = "Shelly BLU Button1" device_type = "BLU Button1" elif name.startswith("SBDW"): manufacturer = "Shelly" name = "Shelly BLU Door/Window" device_type = "BLU Door/Window" else: manufacturer = None device_type = "BTHome sensor" if manufacturer: self.set_device_manufacturer(manufacturer) # Get device information from local name and identifier self.set_device_name(f"{name} {identifier}") self.set_title(f"{name} {identifier}") self.set_device_type(device_type) if self.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY: bthome_mac = bytes.fromhex(mac_readable.replace(":", "")) # Decode encrypted payload try: payload = self._decrypt_bthome( service_info, payload, bthome_mac, sw_version, adv_info ) except (ValueError, TypeError): return True return self._parse_payload(payload, sw_version, service_info.time) def _skip_old_or_duplicated_advertisement( self, new_packet_id: float, adv_time: float ) -> bool: """ Detect duplicated or older packets Devices may send duplicated advertisements or advertisements order can change when passing through a proxy. If more than 4 seconds pass since the last advertisement assume it is a new packet even if it has the same packet id. Packet id rollover at 255 to 0, validate that the difference between last packet id and new packet id is less than 64. This assumes device is not sending more than 16 advertisements per second. """ last_packet_id = self.packet_id # no history, first packet, don't discard packet if last_packet_id is None or self.last_service_info is None: _LOGGER.debug( "%s: First packet, not filtering packet_id %i", self.title, new_packet_id, ) return False # more than 4 seconds since last packet, don't discard packet if adv_time - self.last_service_info.time > 4: _LOGGER.debug( "%s: Not filtering packet_id, more than 4 seconds since last packet. " "New time: %i, Old time: %i", self.title, adv_time, self.last_service_info.time, ) return False # distance between new packet and old packet is less then 64 if (new_packet_id > last_packet_id and new_packet_id - last_packet_id < 64) or ( new_packet_id < last_packet_id and new_packet_id + 256 - last_packet_id < 64 ): return False # discard packet (new_packet_id=last_packet_id or older packet) _LOGGER.debug( "%s: New packet_id %i indicates an older packet (previous packet_id %i). " "BLE advertisement will be skipped", self.title, new_packet_id, last_packet_id, ) return True def _parse_payload(self, payload: bytes, sw_version: int, adv_time: float) -> bool: payload_length = len(payload) next_obj_start = 0 prev_obj_meas_type = 0 result = False measurements: list[dict[str, Any]] = [] postfix_dict: dict[str, int] = {} obj_data_format: str | int # Create a list with all individual objects while payload_length >= next_obj_start + 1: obj_start = next_obj_start if sw_version == 1: # BTHome V1 obj_meas_type = payload[obj_start + 1] obj_control_byte = payload[obj_start] obj_data_length = (obj_control_byte >> 0) & 31 # 5 bits (0-4) obj_data_format = (obj_control_byte >> 5) & 7 # 3 bits (5-7) obj_data_start = obj_start + 2 next_obj_start = obj_start + obj_data_length + 1 else: # BTHome V2 obj_meas_type = payload[obj_start] if prev_obj_meas_type > obj_meas_type: _LOGGER.warning( "%s: BTHome device is not sending object ids in numerical order (from low " "to high object id). This can cause issues with your BTHome receiver, " "payload: %s", self.title, payload.hex(), ) if obj_meas_type not in MEAS_TYPES: _LOGGER.debug( "%s: Invalid Object ID found in payload: %s", self.title, payload.hex(), ) break prev_obj_meas_type = obj_meas_type obj_data_format = MEAS_TYPES[obj_meas_type].data_format if obj_data_format in ["raw", "string"]: obj_data_length = payload[obj_start + 1] obj_data_start = obj_start + 2 else: obj_data_length = MEAS_TYPES[obj_meas_type].data_length obj_data_start = obj_start + 1 next_obj_start = obj_data_start + obj_data_length if obj_data_length == 0: _LOGGER.debug( "%s: Invalid payload data length found with length 0, payload: %s", self.title, payload.hex(), ) continue if payload_length < next_obj_start: _LOGGER.debug( "%s: Invalid payload data length, payload: %s", self.title, payload.hex(), ) break # Filter BLE advertisements with packet_id that has already been parsed. if obj_meas_type == 0: new_packet_id = parse_uint(payload[obj_data_start:next_obj_start]) if self._skip_old_or_duplicated_advertisement(new_packet_id, adv_time): break self.packet_id = new_packet_id measurements.append( { "data format": obj_data_format, "data length": obj_data_length, "measurement type": obj_meas_type, "measurement data": payload[obj_data_start:next_obj_start], "device id": None, } ) # Get a list of measurement types that are included more than once. seen_meas_formats = set() dup_meas_formats = set() for meas in measurements: if meas["measurement type"] in MEAS_TYPES: meas_format = MEAS_TYPES[meas["measurement type"]].meas_format if meas_format in seen_meas_formats: dup_meas_formats.add(meas_format) else: seen_meas_formats.add(meas_format) # Parse each object into readable information for meas in measurements: if meas["measurement type"] not in MEAS_TYPES: _LOGGER.debug( "%s: UNKNOWN measurement type %s in BTHome BLE payload! Adv: %s", self.title, meas["measurement type"], payload.hex(), ) continue meas_type = MEAS_TYPES[meas["measurement type"]] meas_format = meas_type.meas_format meas_factor = meas_type.factor if meas_type.meas_format in dup_meas_formats: # Add a postfix for advertisements with multiple measurements of the same type postfix_counter = postfix_dict.get(meas_format, 0) + 1 postfix_dict[meas_format] = postfix_counter postfix = f"_{postfix_counter}" else: postfix = "" value: None | str | int | float | datetime if meas["data format"] == 0 or meas["data format"] == "unsigned_integer": value = parse_uint(meas["measurement data"], meas_factor) elif meas["data format"] == 1 or meas["data format"] == "signed_integer": value = parse_int(meas["measurement data"], meas_factor) elif meas["data format"] == 2 or meas["data format"] == "float": value = parse_float(meas["measurement data"], meas_factor) elif meas["data format"] == 3 or meas["data format"] == "string": value = parse_string(meas["measurement data"]) elif meas["data format"] == 4 or meas["data format"] == "raw": value = parse_raw(meas["measurement data"]) elif meas["data format"] == 5 or meas["data format"] == "timestamp": value = parse_timestamp(meas["measurement data"]) else: _LOGGER.error( "%s: UNKNOWN dataobject in BTHome BLE payload! Adv: %s", self.title, payload.hex(), ) continue if value is not None: if ( isinstance(meas_format, BaseSensorDescription) and meas_format.device_class ): self.update_sensor( key=f"{str(meas_format.device_class)}{postfix}", native_unit_of_measurement=meas_format.native_unit_of_measurement, native_value=value, device_class=meas_format.device_class, ) elif ( isinstance(meas_format, BaseBinarySensorDescription) and meas_format.device_class ): self.update_binary_sensor( key=f"{str(meas_format.device_class)}{postfix}", device_class=meas_format.device_class, native_value=bool(value), ) elif isinstance(meas_format, EventDeviceKeys): event_type = parse_event_type( event_device=meas_format, data_obj=meas["measurement data"][0], ) event_properties = parse_event_properties( event_device=meas_format, data_obj=meas["measurement data"][1:], ) if event_type: self.fire_event( key=f"{str(meas_format)}{postfix}", event_type=event_type, event_properties=event_properties, ) result = True else: _LOGGER.debug( "%s: UNKNOWN dataobject in BTHome BLE payload! Adv: %s", self.title, payload.hex(), ) if not result: return False return True def _decrypt_bthome( self, service_info: BluetoothServiceInfoBleak, service_data: bytes, bthome_mac: bytes, sw_version: int, adv_info: int = 65, ) -> bytes: """Decrypt encrypted BTHome BLE advertisements""" if not self.bindkey: self.bindkey_verified = False _LOGGER.debug("%s: Encryption key not set and adv is encrypted", self.title) raise ValueError if not self.bindkey or len(self.bindkey) != 16: self.bindkey_verified = False _LOGGER.error( "%s: Encryption key should be 16 bytes (32 characters) long", self.title ) raise ValueError # check for minimum length of encrypted advertisement if len(service_data) < (12 if sw_version == 1 else 11): _LOGGER.debug( "%s: Invalid data length (for decryption), adv: %s", self.title, service_data.hex(), ) raise ValueError # prepare the data for decryption if sw_version == 1: uuid = b"\x1e\x18" else: uuid = b"\xd2\xfc" + bytes([adv_info]) encrypted_payload = service_data[:-8] last_encryption_counter = self.encryption_counter counter = service_data[-8:-4] new_encryption_counter = parse_uint(counter) mic = service_data[-4:] # nonce: mac [6], uuid16 [2 (v1) or 3 (v2)], counter [4] nonce = b"".join([bthome_mac, uuid, counter]) associated_data = None if sw_version == 1: associated_data = b"\x11" assert self.cipher is not None # nosec # filter advertisements that are exactly the same as the previous advertisement if ( self.last_service_info and service_info.service_data == self.last_service_info.service_data and self.bindkey_verified is True ): _LOGGER.debug( "%s: The service data is the same as the previous service data. Skipping " "this BLE advertisement.", self.title, ) raise ValueError # Filter advertisements with a decreasing encryption counter. # Allow cases where the counter has restarted from 0 # (after reaching the highest number or due to a battery change). # In all other cases, assume the data has been compromised and skip the advertisement. if ( new_encryption_counter < last_encryption_counter and self.bindkey_verified is True and new_encryption_counter >= 100 ): _LOGGER.warning( "%s: The new encryption counter (%i) is lower than the previous value (%i). " "The data might be compromised. BLE advertisement will be skipped.", self.title, new_encryption_counter, last_encryption_counter, ) raise ValueError # decrypt the data try: decrypted_payload = self.cipher.decrypt( nonce, encrypted_payload + mic, associated_data ) except InvalidTag as error: if self.decryption_failed is True: # we only ask for reautentification after the decryption has failed twice. self.bindkey_verified = False else: self.decryption_failed = True _LOGGER.warning("%s: Decryption failed: %s", self.title, error) _LOGGER.debug("%s: mic: %s", self.title, mic.hex()) _LOGGER.debug("%s: nonce: %s", self.title, nonce.hex()) _LOGGER.debug( "%s: encrypted_payload: %s", self.title, encrypted_payload.hex() ) raise ValueError if decrypted_payload is None: self.bindkey_verified = False _LOGGER.error( "%s: Decryption failed for %s, decrypted payload is None", self.title, to_mac(bthome_mac), ) raise ValueError self.decryption_failed = False self.bindkey_verified = True self.encryption_counter = new_encryption_counter return decrypted_payload bthome-ble-3.12.5/src/bthome_ble/py.typed000066400000000000000000000000001477343332500202070ustar00rootroot00000000000000bthome-ble-3.12.5/templates/000077500000000000000000000000001477343332500156315ustar00rootroot00000000000000bthome-ble-3.12.5/templates/CHANGELOG.md.j2000066400000000000000000000012351477343332500177550ustar00rootroot00000000000000# Changelog {%- for version, release in context.history.released.items() %} ## {{ version.as_tag() }} ({{ release.tagged_date.strftime("%Y-%m-%d") }}) {%- for category, commits in release["elements"].items() %} {# Category title: Breaking, Fix, Documentation #} ### {{ category | capitalize }} {# List actual changes in the category #} {%- for commit in commits %} {% if commit is not none and commit.descriptions is defined %} - {{ commit.descriptions[0] | capitalize }} ([`{{ commit.short_hash }}`]({{ commit.hexsha | commit_hash_url }})) {% endif %} {%- endfor %}{# for commit #} {%- endfor %}{# for category, commits #} {%- endfor %}{# for version, release #} bthome-ble-3.12.5/tests/000077500000000000000000000000001477343332500147755ustar00rootroot00000000000000bthome-ble-3.12.5/tests/__init__.py000066400000000000000000000000001477343332500170740ustar00rootroot00000000000000bthome-ble-3.12.5/tests/test_parser_v1.py000066400000000000000000001300751477343332500203160ustar00rootroot00000000000000"""Tests for the parser of BLE advertisements in BTHome V1 format.""" import logging from datetime import datetime, timezone from unittest.mock import patch import pytest from bluetooth_sensor_state_data import SensorUpdate from home_assistant_bluetooth import BluetoothServiceInfoBleak from sensor_state_data import ( BinarySensorDescription, BinarySensorDeviceClass, BinarySensorValue, DeviceKey, Event, SensorDescription, SensorDeviceClass, SensorDeviceInfo, SensorValue, Units, ) from bthome_ble.parser import BTHomeBluetoothDeviceData, EncryptionScheme ADVERTISEMENT_TIME = 1709331995.5181565 KEY_BATTERY = DeviceKey(key="battery", device_id=None) KEY_BINARY_GENERIC = DeviceKey(key="generic", device_id=None) KEY_BINARY_OPENING = DeviceKey(key="opening", device_id=None) KEY_BINARY_POWER = DeviceKey(key="power", device_id=None) KEY_BUTTON = DeviceKey(key="button", device_id=None) KEY_CO2 = DeviceKey(key="carbon_dioxide", device_id=None) KEY_DIMMER = DeviceKey(key="dimmer", device_id=None) KEY_COUNT = DeviceKey(key="count", device_id=None) KEY_DEW_POINT = DeviceKey(key="dew_point", device_id=None) KEY_ENERGY = DeviceKey(key="energy", device_id=None) KEY_HUMIDITY = DeviceKey(key="humidity", device_id=None) KEY_ILLUMINANCE = DeviceKey(key="illuminance", device_id=None) KEY_MASS = DeviceKey(key="mass", device_id=None) KEY_MOISTURE = DeviceKey(key="moisture", device_id=None) KEY_PACKET_ID = DeviceKey(key="packet_id", device_id=None) KEY_PM25 = DeviceKey(key="pm25", device_id=None) KEY_PM10 = DeviceKey(key="pm10", device_id=None) KEY_POWER = DeviceKey(key="power", device_id=None) KEY_PRESSURE = DeviceKey(key="pressure", device_id=None) KEY_SIGNAL_STRENGTH = DeviceKey(key="signal_strength", device_id=None) KEY_TEMPERATURE = DeviceKey(key="temperature", device_id=None) KEY_TIMESTAMP = DeviceKey(key="timestamp", device_id=None) KEY_VOC = DeviceKey(key="volatile_organic_compounds", device_id=None) KEY_VOLTAGE = DeviceKey(key="voltage", device_id=None) @pytest.fixture(autouse=True) def logging_config(caplog): caplog.set_level(logging.DEBUG) @pytest.fixture(autouse=True) def mock_platform(): with patch("sys.platform") as p: p.return_value = "linux" yield p def bytes_to_service_info( payload: bytes, local_name: str, address: str = "00:00:00:00:00:00" ) -> BluetoothServiceInfoBleak: """Convert bytes to service info""" return BluetoothServiceInfoBleak( name=local_name, address=address, rssi=-60, manufacturer_data={}, service_data={"0000181c-0000-1000-8000-00805f9b34fb": payload}, service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"], source="", device=None, advertisement=None, connectable=False, time=ADVERTISEMENT_TIME, tx_power=None, ) def bytes_to_encrypted_service_info( payload: bytes, local_name: str, address: str = "00:00:00:00:00:00" ) -> BluetoothServiceInfoBleak: """Convert bytes to service info""" return BluetoothServiceInfoBleak( name=local_name, address=address, rssi=-60, manufacturer_data={}, service_data={"0000181e-0000-1000-8000-00805f9b34fb": payload}, service_uuids=["0000181e-0000-1000-8000-00805f9b34fb"], source="", device=None, advertisement=None, connectable=False, time=ADVERTISEMENT_TIME, tx_power=None, ) def test_can_create(): BTHomeBluetoothDeviceData() def test_encryption_key_needed(): """Test that we can detect that an encryption key is needed.""" data_string = b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99' advertisement = bytes_to_encrypted_service_info( payload=data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData() assert device.supported(advertisement) assert device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY assert not device.bindkey_verified def test_encryption_no_key_needed(): """Test that we can detect that no encryption key is needed.""" data_string = b"\x02\x00\x0c\x04\x04\x13\x8a\x01" advertisement = bytes_to_service_info( payload=data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData() assert device.supported(advertisement) assert device.encryption_scheme == EncryptionScheme.NONE assert not device.bindkey_verified def test_bindkey_wrong(): """Test BTHome parser with wrong encryption key.""" bindkey = "814aac74c4f17b6c1581e1ab87816b99" data_string = b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99' advertisement = bytes_to_encrypted_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert not device.bindkey_verified assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="BTHome sensor", sw_version="BTHome BLE v1 (encrypted)", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( name="Signal Strength", device_key=KEY_SIGNAL_STRENGTH, native_value=-60 ), }, ) def test_bindkey_correct(): """Test BTHome parser with correct encryption key.""" bindkey = "231d39c1d7cc1ab1aee224cd096db932" data_string = b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99' advertisement = bytes_to_encrypted_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert device.bindkey_verified assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 80A5", devices={ None: SensorDeviceInfo( name="TEST DEVICE 80A5", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1 (encrypted)", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_HUMIDITY: SensorDescription( device_key=KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=25.06 ), KEY_HUMIDITY: SensorValue( device_key=KEY_HUMIDITY, name="Humidity", native_value=50.55 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bindkey_verified_can_be_unset(): """Test BTHome parser with wrong encryption key.""" bindkey = "814aac74c4f17b6c1581e1ab87816b99" data_string = b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99' advertisement = bytes_to_encrypted_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) device.bindkey_verified = True assert device.supported(advertisement) assert not device.bindkey_verified def test_bthome_temperature_humidity(caplog): """Test BTHome parser for temperature humidity reading without encryption.""" data_string = b"#\x02\xca\t\x03\x03\xbf\x13" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_HUMIDITY: SensorDescription( device_key=KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=25.06 ), KEY_HUMIDITY: SensorValue( device_key=KEY_HUMIDITY, name="Humidity", native_value=50.55 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_temperature_humidity_battery(caplog): """Test BTHome parser for temperature humidity battery reading.""" data_string = b"#\x02]\t\x03\x03\xb7\x18\x02\x01]" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_HUMIDITY: SensorDescription( device_key=KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_BATTERY: SensorDescription( device_key=KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=23.97 ), KEY_HUMIDITY: SensorValue( device_key=KEY_HUMIDITY, name="Humidity", native_value=63.27 ), KEY_BATTERY: SensorValue( device_key=KEY_BATTERY, name="Battery", native_value=93 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_pressure(caplog): """Test BTHome parser for pressure reading without encryption.""" data_string = b"\x04\x04\x13\x8a\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_PRESSURE: SensorDescription( device_key=KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=Units.PRESSURE_MBAR, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PRESSURE: SensorValue( device_key=KEY_PRESSURE, name="Pressure", native_value=1008.83 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_illuminance(caplog): """Test BTHome parser for illuminance reading without encryption.""" data_string = b"\x04\x05\x13\x8a\x14" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_ILLUMINANCE: SensorDescription( device_key=KEY_ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement=Units.LIGHT_LUX, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_ILLUMINANCE: SensorValue( device_key=KEY_ILLUMINANCE, name="Illuminance", native_value=13460.67 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_mass_kilograms(caplog): """Test BTHome parser for mass reading in kilograms without encryption.""" data_string = b"\x43\x06\xfe\x70" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_MASS: SensorDescription( device_key=KEY_MASS, device_class=SensorDeviceClass.MASS, native_unit_of_measurement=Units.MASS_KILOGRAMS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_MASS: SensorValue( device_key=KEY_MASS, name="Mass", native_value=102.24 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_mass_pounds(caplog): """Test BTHome parser for mass reading in pounds without encryption.""" data_string = b"\x03\x07\x3e\x1d" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_MASS: SensorDescription( device_key=KEY_MASS, device_class=SensorDeviceClass.MASS, native_unit_of_measurement=Units.MASS_POUNDS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_MASS: SensorValue(device_key=KEY_MASS, name="Mass", native_value=74.86), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_dew_point(caplog): """Test BTHome parser for dew point reading without encryption.""" data_string = b"\x23\x08\xca\x06" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_DEW_POINT: SensorDescription( device_key=KEY_DEW_POINT, device_class=SensorDeviceClass.DEW_POINT, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_DEW_POINT: SensorValue( device_key=KEY_DEW_POINT, name="Dew Point", native_value=17.38 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_count(caplog): """Test BTHome parser for counter reading without encryption.""" data_string = b"\x02\x09\x60" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_COUNT: SensorDescription( device_key=KEY_COUNT, device_class=SensorDeviceClass.COUNT, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_COUNT: SensorValue(device_key=KEY_COUNT, name="Count", native_value=96), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_energy(caplog): """Test BTHome parser for energy reading without encryption.""" data_string = b"\x04\n\x13\x8a\x14" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_ENERGY: SensorDescription( device_key=KEY_ENERGY, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=Units.ENERGY_KILO_WATT_HOUR, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_ENERGY: SensorValue( device_key=KEY_ENERGY, name="Energy", native_value=1346.067 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_power(caplog): """Test BTHome parser for power reading without encryption.""" data_string = b"\x04\x0b\x02\x1b\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_POWER: SensorDescription( device_key=KEY_POWER, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=Units.POWER_WATT, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_POWER: SensorValue( device_key=KEY_POWER, name="Power", native_value=69.14 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_voltage(caplog): """Test BThome parser for voltage reading without encryption.""" data_string = b"\x03\x0c\x02\x0c" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_VOLTAGE: SensorDescription( device_key=KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_VOLT, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_VOLTAGE: SensorValue( device_key=KEY_VOLTAGE, name="Voltage", native_value=3.074 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_binary_sensor(caplog): """Test BTHome parser for binary sensor without device class, without encryption.""" data_string = b"\x02\x0f\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, binary_entity_descriptions={ KEY_BINARY_GENERIC: BinarySensorDescription( device_key=KEY_BINARY_GENERIC, device_class=BinarySensorDeviceClass.GENERIC, ), }, binary_entity_values={ KEY_BINARY_GENERIC: BinarySensorValue( device_key=KEY_BINARY_GENERIC, name="Generic", native_value=True ), }, ) def test_bthome_binary_sensor_power(caplog): """Test BTHome parser for binary sensor power without encryption.""" data_string = b"\x02\x10\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, binary_entity_descriptions={ KEY_BINARY_POWER: BinarySensorDescription( device_key=KEY_BINARY_POWER, device_class=BinarySensorDeviceClass.POWER, ), }, binary_entity_values={ KEY_BINARY_POWER: BinarySensorValue( device_key=KEY_BINARY_POWER, name="Power", native_value=True ), }, ) def test_bthome_binary_sensor_opening(caplog): """Test BTHome parser for binary sensor opening without encryption.""" data_string = b"\x02\x11\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, binary_entity_descriptions={ KEY_BINARY_OPENING: BinarySensorDescription( device_key=KEY_BINARY_OPENING, device_class=BinarySensorDeviceClass.OPENING, ), }, binary_entity_values={ KEY_BINARY_OPENING: BinarySensorValue( device_key=KEY_BINARY_OPENING, name="Opening", native_value=False ), }, ) def test_bthome_pm(caplog): """Test BTHome parser for PM2.5 and PM10 reading without encryption.""" data_string = b"\x03\r\x12\x0c\x03\x0e\x02\x1c" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_PM25: SensorDescription( device_key=KEY_PM25, device_class=SensorDeviceClass.PM25, native_unit_of_measurement=( Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ), ), KEY_PM10: SensorDescription( device_key=KEY_PM10, device_class=SensorDeviceClass.PM10, native_unit_of_measurement=( Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ), ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PM25: SensorValue(device_key=KEY_PM25, name="Pm25", native_value=3090), KEY_PM10: SensorValue(device_key=KEY_PM10, name="Pm10", native_value=7170), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_co2(caplog): """Test BTHome parser for CO2 reading without encryption.""" data_string = b"\x03\x12\xe2\x04" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_CO2: SensorDescription( device_key=KEY_CO2, device_class=SensorDeviceClass.CO2, native_unit_of_measurement=Units.CONCENTRATION_PARTS_PER_MILLION, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_CO2: SensorValue( device_key=KEY_CO2, name="Carbon Dioxide", native_value=1250 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_timestamp(caplog): """Test BTHome parser for Unix timestamp (seconds from 1-1-1970).""" data_string = b"\xa5\x50\x5d\x39\x61\x64" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_TIMESTAMP: SensorDescription( device_key=KEY_TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TIMESTAMP: SensorValue( device_key=KEY_TIMESTAMP, name="Timestamp", native_value=datetime(2023, 5, 14, 19, 41, 17, tzinfo=timezone.utc), ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_voc(caplog): """Test BTHome parser for VOC reading without encryption.""" data_string = b"\x03\x133\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_VOC: SensorDescription( device_key=KEY_VOC, device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, native_unit_of_measurement=Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_VOC: SensorValue( device_key=KEY_VOC, name="Volatile Organic Compounds", native_value=307 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_moisture(caplog): """Test BTHome parser for moisture reading from b-parasite sensor.""" data_string = b"\x03\x14\x02\x0c" advertisement = bytes_to_service_info( data_string, local_name="prst", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="b-parasite 18B2", devices={ None: SensorDeviceInfo( name="b-parasite 18B2", manufacturer="b-parasite", model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_MOISTURE: SensorDescription( device_key=KEY_MOISTURE, device_class=SensorDeviceClass.MOISTURE, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_MOISTURE: SensorValue( device_key=KEY_MOISTURE, name="Moisture", native_value=30.74 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_event_button_long_press(caplog): """Test BTHome parser for an event of a long press on a button without encryption.""" data_string = b"\x02\x3a\x04" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, events={ KEY_BUTTON: Event( device_key=KEY_BUTTON, name="Button", event_type="long_press", event_properties=None, ), }, ) def test_bthome_event_dimmer_rotate_left_3_steps(caplog): """Test BTHome parser for an event rotating a dimmer 3 steps left.""" data_string = b"\x03\x3c\x01\x03" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, events={ KEY_DIMMER: Event( device_key=KEY_DIMMER, name="Dimmer", event_type="rotate_left", event_properties={"steps": 3}, ), }, ) def test_bthome_multiple_uuids(caplog): """Test BTHome parser for a device that broadcasts multiple uuids.""" advertisement = BluetoothServiceInfoBleak( name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", rssi=-60, manufacturer_data={}, service_data={ "0000181a-0000-1000-8000-00805f9b34fb": b"\xc4$\x818\xc1\xa4V\x08\x83\x18\xbf", "0000fe95-0000-1000-8000-00805f9b34fb": b"0X[\x05\x01\xc4$\x818\xc1\xa4(\x01\x00", "0000181c-0000-1000-8000-00805f9b34fb": b"\x02\x00\xb4\x02\x10\x00\x03\x0c\xbe\x0b", }, service_uuids=[ "0000181a-0000-1000-8000-00805f9b34fb", "0000fe95-0000-1000-8000-00805f9b34fb", "0000181c-0000-1000-8000-00805f9b34fb", ], source="", device=None, advertisement=None, connectable=False, time=ADVERTISEMENT_TIME, tx_power=None, ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="BTHome sensor", sw_version="BTHome BLE v1", hw_version=None, ) }, entity_descriptions={ KEY_PACKET_ID: SensorDescription( device_key=KEY_PACKET_ID, device_class=SensorDeviceClass.PACKET_ID, native_unit_of_measurement=None, ), KEY_VOLTAGE: SensorDescription( device_key=KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_VOLT, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PACKET_ID: SensorValue( device_key=KEY_PACKET_ID, name="Packet Id", native_value=180 ), KEY_VOLTAGE: SensorValue( device_key=KEY_VOLTAGE, name="Voltage", native_value=3.006 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, binary_entity_descriptions={ DeviceKey(key="power", device_id=None): BinarySensorDescription( device_key=DeviceKey(key="power", device_id=None), device_class=BinarySensorDeviceClass.POWER, ) }, binary_entity_values={ DeviceKey(key="power", device_id=None): BinarySensorValue( device_key=DeviceKey(key="power", device_id=None), name="Power", native_value=False, ) }, ) bthome-ble-3.12.5/tests/test_parser_v2.py000066400000000000000000004362001477343332500203160ustar00rootroot00000000000000"""Tests for the parser of BLE advertisements in BTHome V2 format.""" import logging from datetime import datetime, timezone from unittest.mock import patch import pytest from bluetooth_sensor_state_data import SensorUpdate from home_assistant_bluetooth import BluetoothServiceInfoBleak from sensor_state_data import ( BinarySensorDescription, BinarySensorDeviceClass, BinarySensorValue, DeviceKey, Event, SensorDescription, SensorDeviceClass, SensorDeviceInfo, SensorValue, Units, ) from bthome_ble.const import ExtendedSensorDeviceClass from bthome_ble.parser import BTHomeBluetoothDeviceData, EncryptionScheme ADVERTISEMENT_TIME = 1709331995.5181565 KEY_ACCELERATION = DeviceKey(key="acceleration", device_id=None) KEY_BATTERY = DeviceKey(key="battery", device_id=None) KEY_BINARY_GENERIC = DeviceKey(key="generic", device_id=None) KEY_BINARY_OPENING = DeviceKey(key="opening", device_id=None) KEY_BINARY_POWER = DeviceKey(key="power", device_id=None) KEY_BINARY_WINDOW = DeviceKey(key="window", device_id=None) KEY_BUTTON = DeviceKey(key="button", device_id=None) KEY_CO2 = DeviceKey(key="carbon_dioxide", device_id=None) KEY_DIMMER = DeviceKey(key="dimmer", device_id=None) KEY_DISTANCE = DeviceKey(key="distance", device_id=None) KEY_COUNT = DeviceKey(key="count", device_id=None) KEY_CURRENT = DeviceKey(key="current", device_id=None) KEY_DEW_POINT = DeviceKey(key="dew_point", device_id=None) KEY_DURATION = DeviceKey(key="duration", device_id=None) KEY_ENERGY = DeviceKey(key="energy", device_id=None) KEY_GAS = DeviceKey(key="gas", device_id=None) KEY_GYROSCOPE = DeviceKey(key="gyroscope", device_id=None) KEY_HUMIDITY = DeviceKey(key="humidity", device_id=None) KEY_ILLUMINANCE = DeviceKey(key="illuminance", device_id=None) KEY_MASS = DeviceKey(key="mass", device_id=None) KEY_MOISTURE = DeviceKey(key="moisture", device_id=None) KEY_PACKET_ID = DeviceKey(key="packet_id", device_id=None) KEY_PM25 = DeviceKey(key="pm25", device_id=None) KEY_PM10 = DeviceKey(key="pm10", device_id=None) KEY_POWER = DeviceKey(key="power", device_id=None) KEY_PRESSURE = DeviceKey(key="pressure", device_id=None) KEY_RAW = DeviceKey(key="raw", device_id=None) KEY_ROTATION = DeviceKey(key="rotation", device_id=None) KEY_SIGNAL_STRENGTH = DeviceKey(key="signal_strength", device_id=None) KEY_SPEED = DeviceKey(key="speed", device_id=None) KEY_TEMPERATURE = DeviceKey(key="temperature", device_id=None) KEY_TEXT = DeviceKey(key="text", device_id=None) KEY_TIMESTAMP = DeviceKey(key="timestamp", device_id=None) KEY_UV_INDEX = DeviceKey(key="uv_index", device_id=None) KEY_VOC = DeviceKey(key="volatile_organic_compounds", device_id=None) KEY_VOLTAGE = DeviceKey(key="voltage", device_id=None) KEY_VOLUME = DeviceKey(key="volume", device_id=None) KEY_VOLUME_FLOW_RATE = DeviceKey(key="volume_flow_rate", device_id=None) KEY_VOLUME_STORAGE = DeviceKey(key="volume_storage", device_id=None) KEY_WATER = DeviceKey(key="water", device_id=None) KEY_CONDUCTIVITY = DeviceKey(key="conductivity", device_id=None) @pytest.fixture(autouse=True) def logging_config(caplog): caplog.set_level(logging.DEBUG) @pytest.fixture(autouse=True) def mock_platform(): with patch("sys.platform") as p: p.return_value = "linux" yield p def bytes_to_service_info( payload: bytes, local_name: str, address: str = "00:00:00:00:00:00", time: float = ADVERTISEMENT_TIME, ) -> BluetoothServiceInfoBleak: """Convert bytes to service info""" return BluetoothServiceInfoBleak( name=local_name, address=address, rssi=-60, manufacturer_data={}, service_data={"0000fcd2-0000-1000-8000-00805f9b34fb": payload}, service_uuids=[], source="", device=None, advertisement=None, connectable=False, time=time, tx_power=None, ) def test_can_create(): BTHomeBluetoothDeviceData() def test_encryption_key_needed(): """Test that we can detect that an encryption key is needed.""" data_string = ( b"\x41\xd2\xfc\xef\xe5\x2e\xb2\x12\xd6\x00\x11\x22\x33\xbc\x38\xc9\x66" ) advertisement = bytes_to_service_info( payload=data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData() assert device.supported(advertisement) assert device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY assert not device.bindkey_verified def test_encryption_no_key_needed(): """Test that we can detect that no encryption key is needed.""" data_string = b"\x40\x04\x13\x8a\x01" advertisement = bytes_to_service_info( payload=data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData() assert device.supported(advertisement) assert device.encryption_scheme == EncryptionScheme.NONE assert not device.bindkey_verified def test_sleepy_device(): """Test that we can detect that a device that doesn't update regularly.""" data_string = b"\x44\x04\x13\x8a\x01" advertisement = bytes_to_service_info( payload=data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData() assert device.supported(advertisement) assert device.sleepy_device def test_no_sleepy_device(): """Test that we can detect that a device that updates regularly.""" data_string = b"\x40\x04\x13\x8a\x01" advertisement = bytes_to_service_info( payload=data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData() assert device.supported(advertisement) assert not device.sleepy_device def test_mac_as_name(): """ A sensor without a name gets its MAC address as name from BluetoothServiceInfo. Test that this sensor has BTHome sensor + identifier as its name. """ data_string = b"\x40\x04\x13\x8a\x01" advertisement = bytes_to_service_info( payload=data_string, local_name="A4:C1:38:8D:18:B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData() assert device.supported(advertisement) assert device.update(advertisement) == SensorUpdate( title="BTHome sensor 18B2", devices={ None: SensorDeviceInfo( name="BTHome sensor 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PRESSURE: SensorDescription( device_key=KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=Units.PRESSURE_MBAR, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PRESSURE: SensorValue( device_key=KEY_PRESSURE, name="Pressure", native_value=1008.83 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_has_incorrect_version(): """Test that we can detect a non-existing version v7.""" data_string = b"\xe1\x02\x00\x0c\x04\x04\x13\x8a\x01" advertisement = bytes_to_service_info( payload=data_string, local_name="A4:C1:38:8D:18:B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData() assert not device.supported(advertisement) def test_bindkey_wrong(): """Test BTHome parser with wrong encryption key.""" bindkey = "814aac74c4f17b6c1581e1ab87816b99" data_string = ( b"\x41\xd2\xfc\xef\xe5\x2e\xb2\x12\xd6\x00\x11\x22\x33\xbc\x38\xc9\x66" ) advertisement = bytes_to_service_info( data_string, local_name="Test Sensor", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert not device.bindkey_verified assert device.decryption_failed assert device.update(advertisement) == SensorUpdate( title="Test Sensor 18B2", devices={ None: SensorDeviceInfo( name="Test Sensor 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2 (encrypted)", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( name="Signal Strength", device_key=KEY_SIGNAL_STRENGTH, native_value=-60 ), }, ) def test_bindkey_correct(): """Test BTHome parser with correct encryption key.""" bindkey = "231d39c1d7cc1ab1aee224cd096db932" data_string = b"\x41\xa4\x72\x66\xc9\x5f\x73\x00\x11\x22\x33\x78\x23\x72\x14" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert device.bindkey_verified assert not device.decryption_failed assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 80A5", devices={ None: SensorDeviceInfo( name="TEST DEVICE 80A5", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2 (encrypted)", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_HUMIDITY: SensorDescription( device_key=KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=25.06 ), KEY_HUMIDITY: SensorValue( device_key=KEY_HUMIDITY, name="Humidity", native_value=50.55 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_incorrect_bindkey_length(caplog): """Test BTHome parser with incorrect encryption key length.""" bindkey = ( "231d39c1d7cc1ab1aee224cd096db932231d39c1d7cc1ab1" # 24 bytes instead of 16 ) data_string = b"\x41\xa4\x72\x66\xc9\x5f\x73\x00\x11\x22\x33\x78\x23\x72\x14" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert not device.bindkey_verified assert device.decryption_failed assert ( "TEST DEVICE 80A5: Encryption key should be 16 bytes (32 characters) long" in caplog.text ) def test_bindkey_set_late(): """Test BTHome parser with correct encryption key set after the device is created.""" bindkey = "231d39c1d7cc1ab1aee224cd096db932" data_string = b"\x41\xa4\x72\x66\xc9\x5f\x73\x00\x11\x22\x33\x78\x23\x72\x14" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) device = BTHomeBluetoothDeviceData() device.set_bindkey(bytes.fromhex(bindkey)) assert device.supported(advertisement) assert device.bindkey_verified assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 80A5", devices={ None: SensorDeviceInfo( name="TEST DEVICE 80A5", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2 (encrypted)", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_HUMIDITY: SensorDescription( device_key=KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=25.06 ), KEY_HUMIDITY: SensorValue( device_key=KEY_HUMIDITY, name="Humidity", native_value=50.55 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bindkey_verified_can_be_unset(): """Test BTHome parser with wrong encryption key.""" bindkey = "814aac74c4f17b6c1581e1ab87816b99" data_string = b"\x41\xa4\x72\x66\xc9\x5f\x73\x00\x11\x22\x33\x78\x23\x72\x14" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) device.bindkey_verified = True device.decryption_failed = False assert device.supported(advertisement) # the first advertisement will fail decryption, but we don't ask to reauth yet assert device.bindkey_verified assert device.decryption_failed data_string = b"\x41\xa4\x72\x66\xc9\x5f\x73\x01\x11\x22\x33\x78\x23\x72\x14" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", ) assert device.supported(advertisement) # the second advertisement will fail decryption again, but now we ask to reauth assert device.decryption_failed assert not device.bindkey_verified def test_same_service_data(caplog): """Test BTHome parser with the same service data.""" bindkey = "231d39c1d7cc1ab1aee224cd096db932" data_string = b"\x41\xe4\x45\xf3\xc9\x96\x2b\x33\x22\x11\x00\x6c\x7c\x45\x19" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert device.bindkey_verified assert device.encryption_counter == 1122867 data_string = b"\x41\xe4\x45\xf3\xc9\x96\x2b\x33\x22\x11\x00\x6c\x7c\x45\x19" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) assert device.supported(advertisement) assert device.bindkey_verified assert device.encryption_counter == 1122867 assert ( "TEST DEVICE 80A5: The service data is the same as the previous service data. " "Skipping this BLE advertisement." in caplog.text ) def test_increasing_encryption_counter(caplog): """Test BTHome parser with increasing encryption counter.""" bindkey = "231d39c1d7cc1ab1aee224cd096db932" data_string = b"\x41\xe4\x45\xf3\xc9\x96\x2b\x33\x22\x11\x00\x6c\x7c\x45\x19" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert device.bindkey_verified assert device.encryption_counter == 1122867 data_string = b"\x41\x3e\x93\x2c\xc7\x17\x5f\x34\x22\x11\x00\x55\x38\x76\xaf" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) assert device.supported(advertisement) assert device.bindkey_verified assert device.encryption_counter == 1122868 def test_decreasing_encryption_counter(caplog): """Test BTHome parser with decreasing encryption counter.""" bindkey = "231d39c1d7cc1ab1aee224cd096db932" data_string = b"\x41\xe4\x45\xf3\xc9\x96\x2b\x33\x22\x11\x00\x6c\x7c\x45\x19" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert device.bindkey_verified assert device.encryption_counter == 1122867 data_string = b"\x41\x72\x3d\x30\x35\xfb\x88\x32\x22\x11\x00\x9e\x74\x14\xc0" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) assert device.supported(advertisement) assert device.bindkey_verified # encryption counter should not be updated as it is lower assert device.encryption_counter == 1122867 assert ( "TEST DEVICE 80A5: The new encryption counter (1122866) is lower than the previous value " "(1122867). The data might be compromised. BLE advertisement will be skipped." in caplog.text ) def test_reset_encryption_counter(caplog): """Test BTHome parser during reset of the encryption counter.""" bindkey = "231d39c1d7cc1ab1aee224cd096db932" data_string = b"\x41\xba\x0d\xb0\x7a\xee\xf6\xff\xff\xff\xff\x21\xfd\x46\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert device.bindkey_verified assert device.encryption_counter == 4294967295 data_string = b"\x41\xde\x40\xb5\x0e\x67\x66\x00\x00\x00\x00\xd7\xcb\x95\xde" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) assert device.supported(advertisement) assert device.bindkey_verified assert device.encryption_counter == 0 def test_too_short_encryption_advertisement(caplog): """Test BTHome parser with a too short encrypted advertisement.""" bindkey = "231d39c1d7cc1ab1aee224cd096db932" data_string = b"\x41\xba\x0d\xb0\x7a\xee\xf6\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert "TEST DEVICE 80A5: Invalid data length (for decryption), adv:" in caplog.text def test_identical_packet_id(caplog): """Test BTHome parser for skipping BLE advertisement with identical or older packet id.""" # 1st advertisement data_string = b"\x40\x00\x09" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="Temperature/Humidity sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PACKET_ID: SensorDescription( device_key=KEY_PACKET_ID, device_class=SensorDeviceClass.PACKET_ID, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PACKET_ID: SensorValue( device_key=KEY_PACKET_ID, name="Packet Id", native_value=9 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) assert device.packet_id == 9 assert "ATC 18B2: First packet, not filtering packet_id 9" in caplog.text # advertisement with the same packet id device.update(advertisement) assert device.packet_id == 9 assert ( "ATC 18B2: New packet_id 9 indicates an older packet (previous packet_id 9). " "BLE advertisement will be skipped" in caplog.text ) # advertisement packet id decreased by one, should be dropped data_string_2 = b"\x40\x00\x08" advertisement_2 = bytes_to_service_info( data_string_2, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) assert device.update(advertisement_2) assert device.packet_id == 9 assert ( "ATC 18B2: New packet_id 8 indicates an older packet (previous packet_id 9). " "BLE advertisement will be skipped" in caplog.text ) # advertisement more than 4 seconds from last advertisement advertisement_3 = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", time=ADVERTISEMENT_TIME + 5, ) assert device.update(advertisement_3) assert device.packet_id == 9 assert ( "ATC 18B2: Not filtering packet_id, more than 4 seconds since last packet." in caplog.text ) def test_increasing_packet_id(caplog): """Test BTHome parser for BLE advertisement with increasing packet id.""" # start with 189 data_string = b"\x40\x00\xbd" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() device.update(advertisement) assert device.packet_id == 189 # increase by 1 to 190 data_string_2 = b"\x40\x00\xbe" advertisement_2 = bytes_to_service_info( data_string_2, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) assert device.update(advertisement_2) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="Temperature/Humidity sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PACKET_ID: SensorDescription( device_key=KEY_PACKET_ID, device_class=SensorDeviceClass.PACKET_ID, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PACKET_ID: SensorValue( device_key=KEY_PACKET_ID, name="Packet Id", native_value=190 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) assert device.packet_id == 190 # increast by 63 to 253 data_string_3 = b"\x40\x00\xfd" advertisement_3 = bytes_to_service_info( data_string_3, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device.update(advertisement_3) assert device.packet_id == 253 # increast by 50, rollover to 47 data_string_4 = b"\x40\x00\x2f" advertisement_4 = bytes_to_service_info( data_string_4, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device.update(advertisement_4) assert device.packet_id == 47 def test_bthome_wrong_object_id(caplog): """Test BTHome parser for a non-existing Object ID xFE.""" data_string = b"\x40\xfe\xca\x09" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="Temperature/Humidity sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_battery_wrong_object_id_humidity(caplog): """ Test BTHome parser for battery, wrong object id and humidity reading. Should only return the battery reading, as humidity is after wrong object id. """ data_string = b"\x40\x01\x5d\xfe\x5d\x09\x03\xb7\x18" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="Temperature/Humidity sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_BATTERY: SensorDescription( device_key=KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_BATTERY: SensorValue( device_key=KEY_BATTERY, name="Battery", native_value=93 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_with_mac(caplog): """Test BTHome parser for pressure reading mac address in payload.""" data_string = b"\x42\xb2\x18\x8d\x38\xc1\xa4\x04\x13\x8a\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PRESSURE: SensorDescription( device_key=KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=Units.PRESSURE_MBAR, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PRESSURE: SensorValue( device_key=KEY_PRESSURE, name="Pressure", native_value=1008.83 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_with_mac_encrypted(): """Test BTHome parser with mac address in payload plus encryption.""" bindkey = "231d39c1d7cc1ab1aee224cd096db932" data = b"\x43\xa5\x80\x8f\xe6\x48\x54\xf1\x96\xc0\x6f\xfb\x49\x00\x11\x22\x33\xf0\x8e\xcb\xde" advertisement = bytes_to_service_info( data, local_name="TEST DEVICE", address="54:48:E6:8F:80:A5", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert device.bindkey_verified assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 80A5", devices={ None: SensorDeviceInfo( name="TEST DEVICE 80A5", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2 (encrypted)", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_HUMIDITY: SensorDescription( device_key=KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=25.06 ), KEY_HUMIDITY: SensorValue( device_key=KEY_HUMIDITY, name="Humidity", native_value=50.55 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_temperature_humidity(caplog): """Test BTHome parser for temperature humidity reading without encryption.""" data_string = b"\x40\x02\xca\x09\x03\xbf\x13" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="Temperature/Humidity sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_HUMIDITY: SensorDescription( device_key=KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=25.06 ), KEY_HUMIDITY: SensorValue( device_key=KEY_HUMIDITY, name="Humidity", native_value=50.55 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_packet_id_temperature_humidity_battery(caplog): """Test BTHome parser for packet_id, temperature, humidity and battery reading.""" data_string = b"\x40\x00\x09\x01\x5d\x02\x5d\x09\x03\xb7\x18" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="Temperature/Humidity sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PACKET_ID: SensorDescription( device_key=KEY_PACKET_ID, device_class=SensorDeviceClass.PACKET_ID, native_unit_of_measurement=None, ), KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_HUMIDITY: SensorDescription( device_key=KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_BATTERY: SensorDescription( device_key=KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PACKET_ID: SensorValue( device_key=KEY_PACKET_ID, name="Packet Id", native_value=9 ), KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=23.97 ), KEY_HUMIDITY: SensorValue( device_key=KEY_HUMIDITY, name="Humidity", native_value=63.27 ), KEY_BATTERY: SensorValue( device_key=KEY_BATTERY, name="Battery", native_value=93 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_pressure(caplog): """Test BTHome parser for pressure reading without encryption.""" data_string = b"\x40\x04\x13\x8a\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PRESSURE: SensorDescription( device_key=KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=Units.PRESSURE_MBAR, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PRESSURE: SensorValue( device_key=KEY_PRESSURE, name="Pressure", native_value=1008.83 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_illuminance(caplog): """Test BTHome parser for illuminance reading without encryption.""" data_string = b"\x40\x05\x13\x8a\x14" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_ILLUMINANCE: SensorDescription( device_key=KEY_ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement=Units.LIGHT_LUX, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_ILLUMINANCE: SensorValue( device_key=KEY_ILLUMINANCE, name="Illuminance", native_value=13460.67 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_mass_kilograms(caplog): """Test BTHome parser for mass reading in kilograms without encryption.""" data_string = b"\x40\x06\x5e\x1f" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_MASS: SensorDescription( device_key=KEY_MASS, device_class=SensorDeviceClass.MASS, native_unit_of_measurement=Units.MASS_KILOGRAMS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_MASS: SensorValue(device_key=KEY_MASS, name="Mass", native_value=80.3), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_mass_pounds(caplog): """Test BTHome parser for mass reading in pounds without encryption.""" data_string = b"\x40\x07\x3e\x1d" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_MASS: SensorDescription( device_key=KEY_MASS, device_class=SensorDeviceClass.MASS, native_unit_of_measurement=Units.MASS_POUNDS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_MASS: SensorValue(device_key=KEY_MASS, name="Mass", native_value=74.86), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_dew_point(caplog): """Test BTHome parser for dew point reading without encryption.""" data_string = b"\x40\x08\xca\x06" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_DEW_POINT: SensorDescription( device_key=KEY_DEW_POINT, device_class=SensorDeviceClass.DEW_POINT, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_DEW_POINT: SensorValue( device_key=KEY_DEW_POINT, name="Dew Point", native_value=17.38 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_count(caplog): """Test BTHome parser for counter reading without encryption.""" data_string = b"\x40\x09\x60" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_COUNT: SensorDescription( device_key=KEY_COUNT, device_class=SensorDeviceClass.COUNT, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_COUNT: SensorValue(device_key=KEY_COUNT, name="Count", native_value=96), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_energy(caplog): """Test BTHome parser for energy reading without encryption.""" data_string = b"\x40\x0a\x13\x8a\x14" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_ENERGY: SensorDescription( device_key=KEY_ENERGY, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=Units.ENERGY_KILO_WATT_HOUR, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_ENERGY: SensorValue( device_key=KEY_ENERGY, name="Energy", native_value=1346.067 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_power(caplog): """Test BTHome parser for power reading without encryption.""" data_string = b"\x40\x0b\x02\x1b\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_POWER: SensorDescription( device_key=KEY_POWER, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=Units.POWER_WATT, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_POWER: SensorValue( device_key=KEY_POWER, name="Power", native_value=69.14 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_voltage(caplog): """Test BThome parser for voltage reading without encryption.""" data_string = b"\x40\x0c\x02\x0c" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_VOLTAGE: SensorDescription( device_key=KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_VOLT, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_VOLTAGE: SensorValue( device_key=KEY_VOLTAGE, name="Voltage", native_value=3.074 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_binary_sensor(caplog): """Test BTHome parser for binary sensor without device class, without encryption.""" data_string = b"\x40\x0f\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, binary_entity_descriptions={ KEY_BINARY_GENERIC: BinarySensorDescription( device_key=KEY_BINARY_GENERIC, device_class=BinarySensorDeviceClass.GENERIC, ), }, binary_entity_values={ KEY_BINARY_GENERIC: BinarySensorValue( device_key=KEY_BINARY_GENERIC, name="Generic", native_value=True ), }, ) def test_bthome_binary_sensor_power(caplog): """Test BTHome parser for binary sensor power without encryption.""" data_string = b"\x40\x10\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, binary_entity_descriptions={ KEY_BINARY_POWER: BinarySensorDescription( device_key=KEY_BINARY_POWER, device_class=BinarySensorDeviceClass.POWER, ), }, binary_entity_values={ KEY_BINARY_POWER: BinarySensorValue( device_key=KEY_BINARY_POWER, name="Power", native_value=True ), }, ) def test_bthome_binary_sensor_opening(caplog): """Test BTHome parser for binary sensor opening without encryption.""" data_string = b"\x40\x11\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, binary_entity_descriptions={ KEY_BINARY_OPENING: BinarySensorDescription( device_key=KEY_BINARY_OPENING, device_class=BinarySensorDeviceClass.OPENING, ), }, binary_entity_values={ KEY_BINARY_OPENING: BinarySensorValue( device_key=KEY_BINARY_OPENING, name="Opening", native_value=False ), }, ) def test_bthome_binary_sensor_window(caplog): """Test BTHome parser for binary sensor window without encryption.""" data_string = b"\x40\x2d\x01" advertisement = bytes_to_service_info( data_string, local_name="SBDW-002C", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="Shelly BLU Door/Window 18B2", devices={ None: SensorDeviceInfo( name="Shelly BLU Door/Window 18B2", manufacturer="Shelly", model="BLU Door/Window", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, binary_entity_descriptions={ KEY_BINARY_WINDOW: BinarySensorDescription( device_key=KEY_BINARY_WINDOW, device_class=BinarySensorDeviceClass.WINDOW, ), }, binary_entity_values={ KEY_BINARY_WINDOW: BinarySensorValue( device_key=KEY_BINARY_WINDOW, name="Window", native_value=True ), }, ) def test_bthome_pm(caplog): """Test BTHome parser for PM2.5 and PM10 reading without encryption.""" data_string = b"\x40\x0d\x12\x0c\x0e\x02\x1c" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PM25: SensorDescription( device_key=KEY_PM25, device_class=SensorDeviceClass.PM25, native_unit_of_measurement=( Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ), ), KEY_PM10: SensorDescription( device_key=KEY_PM10, device_class=SensorDeviceClass.PM10, native_unit_of_measurement=( Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ), ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PM25: SensorValue(device_key=KEY_PM25, name="Pm25", native_value=3090), KEY_PM10: SensorValue(device_key=KEY_PM10, name="Pm10", native_value=7170), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_co2(caplog): """Test BTHome parser for CO2 reading without encryption.""" data_string = b"\x40\x12\xe2\x04" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_CO2: SensorDescription( device_key=KEY_CO2, device_class=SensorDeviceClass.CO2, native_unit_of_measurement=Units.CONCENTRATION_PARTS_PER_MILLION, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_CO2: SensorValue( device_key=KEY_CO2, name="Carbon Dioxide", native_value=1250 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_voc(caplog): """Test BTHome parser for VOC reading without encryption.""" data_string = b"\x40\x133\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_VOC: SensorDescription( device_key=KEY_VOC, device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, native_unit_of_measurement=Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_VOC: SensorValue( device_key=KEY_VOC, name="Volatile Organic Compounds", native_value=307 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_moisture(caplog): """Test BTHome parser for moisture reading from b-parasite sensor.""" data_string = b"\x40\x14\x02\x0c" advertisement = bytes_to_service_info( data_string, local_name="prst", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="b-parasite 18B2", devices={ None: SensorDeviceInfo( name="b-parasite 18B2", manufacturer="b-parasite", model="Plant sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_MOISTURE: SensorDescription( device_key=KEY_MOISTURE, device_class=SensorDeviceClass.MOISTURE, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_MOISTURE: SensorValue( device_key=KEY_MOISTURE, name="Moisture", native_value=30.74 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_event_button_long_press(caplog): """Test BTHome parser for an event of a long press on a button without encryption.""" data_string = b"\x40\x3a\x04" advertisement = bytes_to_service_info( data_string, local_name="SBBT-002C", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="Shelly BLU Button1 18B2", devices={ None: SensorDeviceInfo( name="Shelly BLU Button1 18B2", manufacturer="Shelly", model="BLU Button1", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, events={ KEY_BUTTON: Event( device_key=KEY_BUTTON, name="Button", event_type="long_press", event_properties=None, ), }, ) def test_bthome_event_triple_button_device(caplog): """ Test BTHome parser for an event of a triple button device where the 2nd button is pressed and the 3rd button is triple pressed. """ data_string = b"\x40\x3a\x00\x3a\x01\x3a\x03" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, events={ DeviceKey(key="button_2", device_id=None): Event( device_key=DeviceKey(key="button_2", device_id=None), name="Button 2", event_type="press", event_properties=None, ), DeviceKey(key="button_3", device_id=None): Event( device_key=DeviceKey(key="button_3", device_id=None), name="Button 3", event_type="triple_press", event_properties=None, ), }, ) def test_bthome_event_button_hold_press(caplog): """Test BTHome parser for an event of holding press on a button without encryption.""" data_string = b"\x40\x3a\x80" advertisement = bytes_to_service_info( data_string, local_name="SBBT-002C", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="Shelly BLU Button1 18B2", devices={ None: SensorDeviceInfo( name="Shelly BLU Button1 18B2", manufacturer="Shelly", model="BLU Button1", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, events={ KEY_BUTTON: Event( device_key=KEY_BUTTON, name="Button", event_type="hold_press", event_properties=None, ), }, ) def test_bthome_event_dimmer_rotate_left_3_steps(caplog): """Test BTHome parser for an event rotating a dimmer 3 steps left.""" data_string = b"\x40\x3c\x01\x03" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, events={ KEY_DIMMER: Event( device_key=KEY_DIMMER, name="Dimmer", event_type="rotate_left", event_properties={"steps": 3}, ), }, ) def test_bthome_rotation(caplog): """Test BTHome parser for rotation.""" data_string = b"\x40\x3f\x02\x0c" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_ROTATION: SensorDescription( device_key=KEY_ROTATION, device_class=SensorDeviceClass.ROTATION, native_unit_of_measurement=Units.DEGREE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_ROTATION: SensorValue( device_key=KEY_ROTATION, name="Rotation", native_value=307.4 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_invalid_button_event(caplog): """Test BTHome parser for an invalid button event.""" data_string = b"\x40\x3a\xfe" advertisement = bytes_to_service_info( data_string, local_name="SBBT-002C", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="Shelly BLU Button1 18B2", devices={ None: SensorDeviceInfo( name="Shelly BLU Button1 18B2", manufacturer="Shelly", model="BLU Button1", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, events={}, ) def test_encrypted_shelly_blu_button_event(caplog): """Test BTHome parser for an encrypted shelly blu button event.""" bindkey = "90bffd73cb6b26ef58a7a8eba9232036" data_string = b"\x45\x0a\xeb\x84\x46\x7f\x85\xba\x01\x00\x00\x31\x88\x78\x8c" advertisement = bytes_to_service_info( data_string, local_name="SBBT-002C", address="B4:35:22:F5:3D:A4", ) device = BTHomeBluetoothDeviceData(bindkey=bytes.fromhex(bindkey)) assert device.supported(advertisement) assert device.bindkey_verified def test_bthome_distance_millimeters(caplog): """Test BTHome parser for distance in millimeters.""" data_string = b"\x40\x40\x0c\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_DISTANCE: SensorDescription( device_key=KEY_DISTANCE, device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=Units.LENGTH_MILLIMETERS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_DISTANCE: SensorValue( device_key=KEY_DISTANCE, name="Distance", native_value=12 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_distance_meters(caplog): """Test BTHome parser for distance in meters.""" data_string = b"\x40\x41\x4e\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_DISTANCE: SensorDescription( device_key=KEY_DISTANCE, device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=Units.LENGTH_METERS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_DISTANCE: SensorValue( device_key=KEY_DISTANCE, name="Distance", native_value=7.8 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_duration(caplog): """Test BTHome parser for duration in seconds.""" data_string = b"\x40\x42\x4e\x34\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_DURATION: SensorDescription( device_key=KEY_DURATION, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=Units.TIME_SECONDS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_DURATION: SensorValue( device_key=KEY_DURATION, name="Duration", native_value=13.39 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_current(caplog): """Test BTHome parser for current in VA.""" data_string = b"\x40\x43\x4e\x34" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_CURRENT: SensorDescription( device_key=KEY_CURRENT, device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=Units.ELECTRIC_CURRENT_AMPERE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_CURRENT: SensorValue( device_key=KEY_CURRENT, name="Current", native_value=13.39 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_speed(caplog): """Test BTHome parser for speed in m/s.""" data_string = b"\x40\x44\x4e\x34" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SPEED: SensorDescription( device_key=KEY_SPEED, device_class=SensorDeviceClass.SPEED, native_unit_of_measurement=Units.SPEED_METERS_PER_SECOND, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SPEED: SensorValue( device_key=KEY_SPEED, name="Speed", native_value=133.9 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_temperature_2(caplog): """Test BTHome parser for temperature with one digit.""" data_string = b"\x40\x45\x11\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=27.3 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_uv_index(caplog): """Test BTHome parser for UV index.""" data_string = b"\x40\x46\x32" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_UV_INDEX: SensorDescription( device_key=KEY_UV_INDEX, device_class=SensorDeviceClass.UV_INDEX, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_UV_INDEX: SensorValue( device_key=KEY_UV_INDEX, name="Uv Index", native_value=5.0 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_volume_liters(caplog): """Test BTHome parser for Volume in Liters.""" data_string = b"\x40\x47\x87\x56" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_VOLUME: SensorDescription( device_key=KEY_VOLUME, device_class=SensorDeviceClass.VOLUME, native_unit_of_measurement=Units.VOLUME_LITERS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_VOLUME: SensorValue( device_key=KEY_VOLUME, name="Volume", native_value=2215.1 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_volume_milliliters(caplog): """Test BTHome parser for Volume in milliliters.""" data_string = b"\x40\x48\xdc\x87" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_VOLUME: SensorDescription( device_key=KEY_VOLUME, device_class=SensorDeviceClass.VOLUME, native_unit_of_measurement=Units.VOLUME_MILLILITERS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_VOLUME: SensorValue( device_key=KEY_VOLUME, name="Volume", native_value=34780 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_volume_flow_rate(caplog): """Test BTHome parser for Volume flow rate in m3 per hour.""" data_string = b"\x40\x49\xdc\x87" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_VOLUME_FLOW_RATE: SensorDescription( device_key=KEY_VOLUME_FLOW_RATE, device_class=SensorDeviceClass.VOLUME_FLOW_RATE, native_unit_of_measurement=Units.VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_VOLUME_FLOW_RATE: SensorValue( device_key=KEY_VOLUME_FLOW_RATE, name="Volume Flow Rate", native_value=34.780, ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_voltage_2(caplog): """Test BThome parser for voltage reading without encryption.""" data_string = b"\x40\x4a\x02\x0c" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_VOLTAGE: SensorDescription( device_key=KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_VOLT, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_VOLTAGE: SensorValue( device_key=KEY_VOLTAGE, name="Voltage", native_value=307.4 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_gas(caplog): """Test BTHome parser for gas reading without encryption.""" data_string = b"\x40\x4b\x13\x8a\x14" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_GAS: SensorDescription( device_key=KEY_GAS, device_class=SensorDeviceClass.GAS, native_unit_of_measurement=Units.VOLUME_CUBIC_METERS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_GAS: SensorValue(device_key=KEY_GAS, name="Gas", native_value=1346.067), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_gas_2(caplog): """Test BTHome parser for gas reading without encryption.""" data_string = b"\x40\x4c\x41\x01\x8a\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_GAS: SensorDescription( device_key=KEY_GAS, device_class=SensorDeviceClass.GAS, native_unit_of_measurement=Units.VOLUME_CUBIC_METERS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_GAS: SensorValue( device_key=KEY_GAS, name="Gas", native_value=25821.505 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_energy_2(caplog): """Test BTHome parser for energy reading without encryption.""" data_string = b"\x40\x4d\x12\x13\x8a\x14" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_ENERGY: SensorDescription( device_key=KEY_ENERGY, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=Units.ENERGY_KILO_WATT_HOUR, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_ENERGY: SensorValue( device_key=KEY_ENERGY, name="Energy", native_value=344593.17 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_volume_liters_2(caplog): """Test BTHome parser for Volume in Liters.""" data_string = b"\x40\x4e\x87\x56\x2a\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_VOLUME: SensorDescription( device_key=KEY_VOLUME, device_class=SensorDeviceClass.VOLUME, native_unit_of_measurement=Units.VOLUME_LITERS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_VOLUME: SensorValue( device_key=KEY_VOLUME, name="Volume", native_value=19551.879 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_volume_water(caplog): """Test BTHome parser for water in Liters.""" data_string = b"\x40\x4f\x87\x56\x2a\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_WATER: SensorDescription( device_key=KEY_WATER, device_class=SensorDeviceClass.WATER, native_unit_of_measurement=Units.VOLUME_LITERS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_WATER: SensorValue( device_key=KEY_WATER, name="Water", native_value=19551.879 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_timestamp(caplog): """Test BTHome parser for Unix timestamp (seconds from 1-1-1970).""" data_string = b"\x44\x50\x5d\x39\x61\x64" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_TIMESTAMP: SensorDescription( device_key=KEY_TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TIMESTAMP: SensorValue( device_key=KEY_TIMESTAMP, name="Timestamp", native_value=datetime(2023, 5, 14, 19, 41, 17, tzinfo=timezone.utc), ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_acceleration(caplog): """Test BTHome parser for acceleration in m/s°.""" data_string = b"\x44\x51\x87\x56" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_ACCELERATION: SensorDescription( device_key=KEY_ACCELERATION, device_class=SensorDeviceClass.ACCELERATION, native_unit_of_measurement=Units.ACCELERATION_METERS_PER_SQUARE_SECOND, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_ACCELERATION: SensorValue( device_key=KEY_ACCELERATION, name="Acceleration", native_value=22.151 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_gyroscope(caplog): """Test BTHome parser for gyroscope in °/s.""" data_string = b"\x44\x52\x87\x56" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_GYROSCOPE: SensorDescription( device_key=KEY_GYROSCOPE, device_class=SensorDeviceClass.GYROSCOPE, native_unit_of_measurement=Units.GYROSCOPE_DEGREES_PER_SECOND, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_GYROSCOPE: SensorValue( device_key=KEY_GYROSCOPE, name="Gyroscope", native_value=22.151 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_text(caplog): """Test BTHome parser for text.""" data_string = b"\x44\x53\x0c\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_TEXT: SensorDescription( device_key=KEY_TEXT, device_class=ExtendedSensorDeviceClass.TEXT, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEXT: SensorValue( device_key=KEY_TEXT, name="Text", native_value="Hello World!" ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_raw(caplog): """Test BTHome parser for raw hex data.""" data_string = b"\x44\x54\x0c\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_RAW: SensorDescription( device_key=KEY_RAW, device_class=ExtendedSensorDeviceClass.RAW, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_RAW: SensorValue( device_key=KEY_RAW, name="Raw", native_value="48656c6c6f20576f726c6421" ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_text_invalid(caplog): """Test BTHome parser for text sensor with invalid format.""" data_string = b"\x44\x53\x01\x87" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_volume_storage(caplog): """Test BTHome parser for volume storage in Liters.""" data_string = b"\x40\x55\x87\x56\x2a\x01" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_VOLUME_STORAGE: SensorDescription( device_key=KEY_VOLUME_STORAGE, device_class=ExtendedSensorDeviceClass.VOLUME_STORAGE, native_unit_of_measurement=Units.VOLUME_LITERS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_VOLUME_STORAGE: SensorValue( device_key=KEY_VOLUME_STORAGE, name="Volume Storage", native_value=19551.879, ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_conductivity(caplog): """Test BTHome parser for conductivity reading from SenseNL sensor.""" data_string = b"\x40\x56\xe8\x03" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_CONDUCTIVITY: SensorDescription( device_key=KEY_CONDUCTIVITY, device_class=SensorDeviceClass.CONDUCTIVITY, native_unit_of_measurement=Units.CONDUCTIVITY, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_CONDUCTIVITY: SensorValue( device_key=KEY_CONDUCTIVITY, name="Conductivity", native_value=1000 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_temperature_1_byte(caplog): """Test BTHome parser for 1 byte temperature reading without encryption.""" data_string = b"\x40\x57\xea" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="Temperature/Humidity sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=-22 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_temperature_1_byte_2(caplog): """Test BTHome parser for 1 byte temperature reading without encryption.""" data_string = b"\x40\x58\xea" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="Temperature/Humidity sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=-7.7 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_count_signed(caplog): """Test BTHome parser for signed counter reading without encryption.""" data_string = b"\x40\x59\xea" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_COUNT: SensorDescription( device_key=KEY_COUNT, device_class=SensorDeviceClass.COUNT, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_COUNT: SensorValue( device_key=KEY_COUNT, name="Count", native_value=-22 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_count_signed_2_bytes(caplog): """Test BTHome parser for signed counter reading without encryption.""" data_string = b"\x40\x5a\xea\xea" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_COUNT: SensorDescription( device_key=KEY_COUNT, device_class=SensorDeviceClass.COUNT, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_COUNT: SensorValue( device_key=KEY_COUNT, name="Count", native_value=-5398 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_count_signed_4_bytes(caplog): """Test BTHome parser for signed counter reading without encryption.""" data_string = b"\x40\x5b\xea\x02\x34\xea" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_COUNT: SensorDescription( device_key=KEY_COUNT, device_class=SensorDeviceClass.COUNT, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_COUNT: SensorValue( device_key=KEY_COUNT, name="Count", native_value=-365690134 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_power_signed(caplog): """Test BTHome parser for signed power reading without encryption.""" data_string = b"\x40\x5c\x02\xfb\xff\xff" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_POWER: SensorDescription( device_key=KEY_POWER, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=Units.POWER_WATT, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_POWER: SensorValue( device_key=KEY_POWER, name="Power", native_value=-12.78 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_current_signed(caplog): """Test BTHome parser for signed current in VA.""" data_string = b"\x40\x5d\x02\xea" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_CURRENT: SensorDescription( device_key=KEY_CURRENT, device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=Units.ELECTRIC_CURRENT_AMPERE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_CURRENT: SensorValue( device_key=KEY_CURRENT, name="Current", native_value=-5.63 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_double_temperature(caplog): """Test BTHome parser for double temperature reading without encryption.""" data_string = b"\x40\x02\xca\x09\x02\xcf\x09" advertisement = bytes_to_service_info( data_string, local_name="A4:C1:38:8D:18:B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="BTHome sensor 18B2", devices={ None: SensorDeviceInfo( name="BTHome sensor 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ DeviceKey(key="temperature_1", device_id=None): SensorDescription( device_key=DeviceKey(key="temperature_1", device_id=None), device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), DeviceKey(key="temperature_2", device_id=None): SensorDescription( device_key=DeviceKey(key="temperature_2", device_id=None), device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ DeviceKey(key="temperature_1", device_id=None): SensorValue( device_key=DeviceKey(key="temperature_1", device_id=None), name="Temperature 1", native_value=25.06, ), DeviceKey(key="temperature_2", device_id=None): SensorValue( device_key=DeviceKey(key="temperature_2", device_id=None), name="Temperature 2", native_value=25.11, ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_triple_temperature_double_humidity_battery(caplog): """ Test BTHome parser for triple temperature, double humidity and single battery reading without encryption. """ data_string = ( b"\x40\x02\xca\x09\x02\xcf\x09\x02\xcf\x08\x03\xb7\x18\x03\xb7\x17\x01\x5d" ) advertisement = bytes_to_service_info( data_string, local_name="A4:C1:38:8D:18:B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="BTHome sensor 18B2", devices={ None: SensorDeviceInfo( name="BTHome sensor 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ DeviceKey(key="temperature_1", device_id=None): SensorDescription( device_key=DeviceKey(key="temperature_1", device_id=None), device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), DeviceKey(key="temperature_2", device_id=None): SensorDescription( device_key=DeviceKey(key="temperature_2", device_id=None), device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), DeviceKey(key="temperature_3", device_id=None): SensorDescription( device_key=DeviceKey(key="temperature_3", device_id=None), device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), DeviceKey(key="humidity_1", device_id=None): SensorDescription( device_key=DeviceKey(key="humidity_1", device_id=None), device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), DeviceKey(key="humidity_2", device_id=None): SensorDescription( device_key=DeviceKey(key="humidity_2", device_id=None), device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_BATTERY: SensorDescription( device_key=KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ DeviceKey(key="temperature_1", device_id=None): SensorValue( device_key=DeviceKey(key="temperature_1", device_id=None), name="Temperature 1", native_value=25.06, ), DeviceKey(key="temperature_2", device_id=None): SensorValue( device_key=DeviceKey(key="temperature_2", device_id=None), name="Temperature 2", native_value=25.11, ), DeviceKey(key="temperature_3", device_id=None): SensorValue( device_key=DeviceKey(key="temperature_3", device_id=None), name="Temperature 3", native_value=22.55, ), DeviceKey(key="humidity_1", device_id=None): SensorValue( device_key=DeviceKey(key="humidity_1", device_id=None), name="Humidity 1", native_value=63.27, ), DeviceKey(key="humidity_2", device_id=None): SensorValue( device_key=DeviceKey(key="humidity_2", device_id=None), name="Humidity 2", native_value=60.71, ), KEY_BATTERY: SensorValue(KEY_BATTERY, name="Battery", native_value=93), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_multiple_uuids(caplog): """Test BTHome parser for a device that broadcasts multiple uuids.""" advertisement = BluetoothServiceInfoBleak( name="ATC_8D18B2", address="A4:C1:38:8D:18:B2", rssi=-60, manufacturer_data={}, service_data={ "0000181a-0000-1000-8000-00805f9b34fb": b"\xc4$\x818\xc1\xa4V\x08\x83\x18\xbf", "0000fe95-0000-1000-8000-00805f9b34fb": b"0X[\x05\x01\xc4$\x818\xc1\xa4(\x01\x00", "0000fcd2-0000-1000-8000-00805f9b34fb": b"\x40\x01\x5d\x02\x5d\x09\x03\xb7\x18", }, service_uuids=[ "0000181a-0000-1000-8000-00805f9b34fb", "0000fe95-0000-1000-8000-00805f9b34fb", "0000fcd2-0000-1000-8000-00805f9b34fb", ], source="", device=None, advertisement=None, connectable=False, time=ADVERTISEMENT_TIME, tx_power=None, ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="ATC 18B2", devices={ None: SensorDeviceInfo( name="ATC 18B2", manufacturer="Xiaomi", model="Temperature/Humidity sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_TEMPERATURE: SensorDescription( device_key=KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, ), KEY_HUMIDITY: SensorDescription( device_key=KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_BATTERY: SensorDescription( device_key=KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_TEMPERATURE: SensorValue( device_key=KEY_TEMPERATURE, name="Temperature", native_value=23.97 ), KEY_HUMIDITY: SensorValue( device_key=KEY_HUMIDITY, name="Humidity", native_value=63.27 ), KEY_BATTERY: SensorValue( device_key=KEY_BATTERY, name="Battery", native_value=93 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_double_voltage_different_object_id(caplog): """ Test BTHome parser for double voltage with different object id. """ data_string = b"@\x00\x01\x0b\x00\x00\x00J\r\t\x013\x0c\xe9\x0c" advertisement = bytes_to_service_info( data_string, local_name="A4:C1:38:8D:18:B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="BTHome sensor 18B2", devices={ None: SensorDeviceInfo( name="BTHome sensor 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PACKET_ID: SensorDescription( device_key=KEY_PACKET_ID, device_class=SensorDeviceClass.PACKET_ID, native_unit_of_measurement=None, ), DeviceKey(key="power", device_id=None): SensorDescription( device_key=DeviceKey(key="power", device_id=None), device_class=SensorDeviceClass.POWER, native_unit_of_measurement=Units.POWER_WATT, ), DeviceKey(key="voltage_1", device_id=None): SensorDescription( device_key=DeviceKey(key="voltage_1", device_id=None), device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_VOLT, ), KEY_BATTERY: SensorDescription( device_key=KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=Units.PERCENTAGE, ), DeviceKey(key="voltage_2", device_id=None): SensorDescription( device_key=DeviceKey(key="voltage_2", device_id=None), device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_VOLT, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PACKET_ID: SensorValue( device_key=KEY_PACKET_ID, name="Packet Id", native_value=1 ), DeviceKey(key="power", device_id=None): SensorValue( device_key=DeviceKey(key="power", device_id=None), name="Power", native_value=0.0, ), DeviceKey(key="voltage_1", device_id=None): SensorValue( device_key=DeviceKey(key="voltage_1", device_id=None), name="Voltage 1", native_value=231.7, ), KEY_BATTERY: SensorValue(KEY_BATTERY, name="Battery", native_value=51), DeviceKey(key="voltage_2", device_id=None): SensorValue( device_key=DeviceKey(key="voltage_2", device_id=None), name="Voltage 2", native_value=3.305, ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_shelly_button(caplog): """ Test BTHome parser with a shelly button. """ data_string = b"@\x00R\x01d:\x01" advertisement = bytes_to_service_info( data_string, local_name="A4:C1:38:8D:18:B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() update = device.update(advertisement) assert device.supported(advertisement) is True assert update == SensorUpdate( title="BTHome sensor 18B2", devices={ None: SensorDeviceInfo( name="BTHome sensor 18B2", model="BTHome sensor", manufacturer=None, sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ DeviceKey(key="packet_id", device_id=None): SensorDescription( device_key=DeviceKey(key="packet_id", device_id=None), device_class=SensorDeviceClass.PACKET_ID, native_unit_of_measurement=None, ), DeviceKey(key="battery", device_id=None): SensorDescription( device_key=DeviceKey(key="battery", device_id=None), device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=Units.PERCENTAGE, ), DeviceKey(key="signal_strength", device_id=None): SensorDescription( device_key=DeviceKey(key="signal_strength", device_id=None), device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ DeviceKey(key="packet_id", device_id=None): SensorValue( device_key=DeviceKey(key="packet_id", device_id=None), name="Packet " "Id", native_value=82, ), DeviceKey(key="battery", device_id=None): SensorValue( device_key=DeviceKey(key="battery", device_id=None), name="Battery", native_value=100, ), DeviceKey(key="signal_strength", device_id=None): SensorValue( device_key=DeviceKey(key="signal_strength", device_id=None), name="Signal " "Strength", native_value=-60, ), }, binary_entity_descriptions={}, binary_entity_values={}, events={ DeviceKey(key="button", device_id=None): Event( device_key=DeviceKey(key="button", device_id=None), name="Button", event_type="press", event_properties=None, ) }, ) def test_bthome_shelly_button_no_press(caplog): """Test BTHome parser for button event followed by empty event.""" data_string = b"\x44\x00\x21\x01\x5e\x3a\x01" advertisement = bytes_to_service_info( data_string, local_name="SBBT-002C", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="Shelly BLU Button1 18B2", devices={ None: SensorDeviceInfo( name="Shelly BLU Button1 18B2", manufacturer="Shelly", model="BLU Button1", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PACKET_ID: SensorDescription( device_key=KEY_PACKET_ID, device_class=SensorDeviceClass.PACKET_ID, native_unit_of_measurement=None, ), KEY_BATTERY: SensorDescription( device_key=KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PACKET_ID: SensorValue( device_key=KEY_PACKET_ID, name="Packet Id", native_value=33 ), KEY_BATTERY: SensorValue(KEY_BATTERY, name="Battery", native_value=94), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, events={ DeviceKey(key="button", device_id=None): Event( device_key=DeviceKey(key="button", device_id=None), name="Button", event_type="press", event_properties=None, ) }, ) data_string = b"\x44\x00\x23\x01\x5e\x3a\x00" advertisement = bytes_to_service_info( data_string, local_name="SBBT-002C", address="A4:C1:38:8D:18:B2" ) assert device.update(advertisement) == SensorUpdate( title="Shelly BLU Button1 18B2", devices={ None: SensorDeviceInfo( name="Shelly BLU Button1 18B2", manufacturer="Shelly", model="BLU Button1", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PACKET_ID: SensorDescription( device_key=KEY_PACKET_ID, device_class=SensorDeviceClass.PACKET_ID, native_unit_of_measurement=None, ), KEY_BATTERY: SensorDescription( device_key=KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=Units.PERCENTAGE, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PACKET_ID: SensorValue( device_key=KEY_PACKET_ID, name="Packet Id", native_value=35 ), KEY_BATTERY: SensorValue(KEY_BATTERY, name="Battery", native_value=94), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, events={}, ) def test_bthome_test(caplog): """Test BTHome parser for acceleration in m/s°.""" data_string = b"@\x00\xa7\x0cx\x0b\x10\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="TEST DEVICE 18B2", devices={ None: SensorDeviceInfo( name="TEST DEVICE 18B2", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ KEY_PACKET_ID: SensorDescription( device_key=KEY_PACKET_ID, device_class=SensorDeviceClass.PACKET_ID, native_unit_of_measurement=None, ), KEY_VOLTAGE: SensorDescription( device_key=KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_VOLT, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ KEY_PACKET_ID: SensorValue( device_key=KEY_PACKET_ID, name="Packet Id", native_value=167 ), KEY_VOLTAGE: SensorValue( device_key=KEY_VOLTAGE, name="Voltage", native_value=2.936 ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, binary_entity_descriptions={ KEY_BINARY_POWER: BinarySensorDescription( device_key=KEY_BINARY_POWER, device_class=BinarySensorDeviceClass.POWER, ), }, binary_entity_values={ KEY_BINARY_POWER: BinarySensorValue( device_key=KEY_BINARY_POWER, name="Power", native_value=False ), }, ) def test_incorrect_bthome_version(caplog): """Test BTHome parser with incorrect BTHome version.""" data_string = b"\x00" advertisement = bytes_to_service_info( data_string, local_name="TEST DEVICE", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() device.update(advertisement) assert device.supported(advertisement) is False assert ( "18B2: Sensor is set to use BTHome version 0, which is not existing. " "Please modify the version in the first byte of the service data" in caplog.text ) def test_bthome_not_sending_object_ids_in_numerical_order(caplog): """Test BTHome parser for not sending object ids in numerical order.""" data_string = b"\x40\x03\xbf\x13\x02\xca\x09" advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() device.update(advertisement) assert ( "ATC 18B2: BTHome device is not sending object ids in numerical order " "(from low to high object id). This can cause issues with your BTHome receiver, " "payload: 03bf1302ca09" in caplog.text ) def test_bthome_invalid_object_payload_data_length(caplog): """Test BTHome parser for object with invalid payload data length.""" data_string = b"\x40\x02\xca\x09\x03\xbf" # missing 1 byte advertisement = bytes_to_service_info( data_string, local_name="ATC_8D18B2", address="A4:C1:38:8D:18:B2" ) device = BTHomeBluetoothDeviceData() device.update(advertisement) assert "ATC 18B2: Invalid payload data length, payload: 02ca0903bf" in caplog.text def test_bthome_direction_precipitation(caplog): """Test BTHome parser for Direction(0x5E) and Precipitation(0x5F) without encryption.""" data_string = b"\x40\x5e\x3f\x75\x5f\x11\xd5" advertisement = bytes_to_service_info( data_string, local_name="SBWS-90CM", address="A4:C1:38:55:AA:55" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="SBWS-90CM AA55", devices={ None: SensorDeviceInfo( name="SBWS-90CM AA55", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ DeviceKey(key="direction", device_id=None): SensorDescription( device_key=DeviceKey(key="direction", device_id=None), device_class=ExtendedSensorDeviceClass.DIRECTION, native_unit_of_measurement=Units.DEGREE, ), DeviceKey(key="precipitation", device_id=None): SensorDescription( device_key=DeviceKey(key="precipitation", device_id=None), device_class=ExtendedSensorDeviceClass.PRECIPITATION, native_unit_of_measurement=Units.LENGTH_MILLIMETERS, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ DeviceKey(key="direction", device_id=None): SensorValue( device_key=DeviceKey(key="direction", device_id=None), name="Direction", native_value=300.15, ), DeviceKey(key="precipitation", device_id=None): SensorValue( device_key=DeviceKey(key="precipitation", device_id=None), name="Precipitation", native_value=54545, ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) def test_bthome_channel(caplog): """Test BTHome parser for Channel(0x60) without encryption.""" data_string = b"\x40\x60\x01" advertisement = bytes_to_service_info( data_string, local_name="SBRC-005B", address="28:DB:A7:B5:D4:03" ) device = BTHomeBluetoothDeviceData() assert device.update(advertisement) == SensorUpdate( title="SBRC-005B D403", devices={ None: SensorDeviceInfo( name="SBRC-005B D403", manufacturer=None, model="BTHome sensor", sw_version="BTHome BLE v2", hw_version=None, ) }, entity_descriptions={ DeviceKey(key="channel", device_id=None): SensorDescription( device_key=DeviceKey(key="channel", device_id=None), device_class=ExtendedSensorDeviceClass.CHANNEL, native_unit_of_measurement=None, ), KEY_SIGNAL_STRENGTH: SensorDescription( device_key=KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), }, entity_values={ DeviceKey(key="channel", device_id=None): SensorValue( device_key=DeviceKey(key="channel", device_id=None), name="Channel", native_value=1, ), KEY_SIGNAL_STRENGTH: SensorValue( device_key=KEY_SIGNAL_STRENGTH, name="Signal Strength", native_value=-60 ), }, ) bthome-ble-3.12.5/tests/test_v1_encryption.py000066400000000000000000000014461477343332500212130ustar00rootroot00000000000000"""Tests for the parser of BLE advertisements in BTHome V1 format.""" import binascii from bthome_ble.bthome_v1_encryption import decrypt_aes_ccm, encrypt_payload def test_encryption_example(): """Test BTHome V1 encryption example.""" data = bytes(bytearray.fromhex("2302CA090303BF13")) # BTHome data (not encrypted) count_id = bytes(bytearray.fromhex("00112233")) # count id (change every message) mac = binascii.unhexlify("5448E68F80A5") # MAC uuid16 = b"\x1e\x18" bindkey = binascii.unhexlify("231d39c1d7cc1ab1aee224cd096db932") payload = encrypt_payload( data=data, mac=mac, uuid16=uuid16, count_id=count_id, key=bindkey ) assert decrypt_aes_ccm(key=bindkey, mac=mac, data=payload) == { "humidity": 50.55, "temperature": 25.06, } bthome-ble-3.12.5/tests/test_v2_encryption.py000066400000000000000000000020171477343332500212070ustar00rootroot00000000000000"""Tests for the parser of BLE advertisements in BTHome V2 format.""" import binascii from bthome_ble.bthome_v2_encryption import decrypt_aes_ccm, encrypt_payload def test_encryption_example(): """Test BTHome V2 encryption example.""" data = bytes(bytearray.fromhex("02CA0903BF13")) # BTHome data (not encrypted) count_id = bytes(bytearray.fromhex("00112233")) # count id (change every message) mac = binascii.unhexlify("5448E68F80A5") # MAC uuid16 = b"\xd2\xfc" sw_version = b"\x41" bindkey = binascii.unhexlify("231d39c1d7cc1ab1aee224cd096db932") encrypted_payload = encrypt_payload( data=data, mac=mac, uuid16=uuid16, sw_version=sw_version, count_id=count_id, key=bindkey, ) assert ( encrypted_payload == b"\xd2\xfc\x41\xa4\x72\x66\xc9\x5f\x73\x00\x11\x22\x33\x78\x23\x72\x14" ) assert decrypt_aes_ccm(key=bindkey, mac=mac, data=encrypted_payload) == { "humidity": 50.55, "temperature": 25.06, }