diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 0000000..b798d29 --- /dev/null +++ b/.cspell.json @@ -0,0 +1,16 @@ +{ + "ignorePaths": [ + "**/node_modules/**", + "**/vscode-extension/**", + "**/.git/**", + "**/.pnpm-lock.json", + ".vscode", + "megalinter", + "package-lock.json", + "report" + ], + "language": "en", + "noConfigSearch": true, + "words": ["megalinter", "oxsecurity"], + "version": "0.2" +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..15a0eb1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +plejd/tests/* +__pycache__ +.vscode +build +dist +*.egg-info +.venv +megalinter-reports/ diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml new file mode 100644 index 0000000..2cb2609 --- /dev/null +++ b/.github/workflows/mega-linter.yml @@ -0,0 +1,184 @@ +# MegaLinter GitHub Action configuration file +# More info at https://megalinter.io +--- +name: MegaLinter + +# Trigger mega-linter at every push. Action will also be visible from +# Pull Requests to main +on: + # Comment this line to trigger action only on pull-requests + # (not recommended if you don't pay for GH Actions) + push: + + pull_request: + branches: + - main + - master + +# Comment env block if you do not want to apply fixes +env: + # Apply linter fixes configuration + # + # When active, APPLY_FIXES must also be defined as environment variable + # (in github/workflows/mega-linter.yml or other CI tool) + APPLY_FIXES: all + + # Decide which event triggers application of fixes in a commit or a PR + # (pull_request, push, all) + APPLY_FIXES_EVENT: pull_request + + # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) + # or posted in a PR (pull_request) + APPLY_FIXES_MODE: commit + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + megalinter: + name: MegaLinter + runs-on: ubuntu-latest + + # Give the default GITHUB_TOKEN write permission to commit and push, comment + # issues, and post new Pull Requests; remove the ones you do not need + permissions: + contents: write + issues: write + pull-requests: write + + steps: + # Git Checkout + - name: Checkout Code + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + + # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to + # improve performance + fetch-depth: 0 + + # MegaLinter + - name: MegaLinter + + # You can override MegaLinter flavor used to have faster performances + # More info at https://megalinter.io/latest/flavors/ + uses: oxsecurity/megalinter/flavors/python@v7 + + id: ml + + # All available variables are described in documentation + # https://megalinter.io/latest/config-file/ + env: + # Validates all source when push on main, else just the git diff with + # main. Override with true if you always want to lint all sources + # + # To validate the entire codebase, set to: + # VALIDATE_ALL_CODEBASE: true + # + # To validate only diff with main, set to: + # VALIDATE_ALL_CODEBASE: >- + # ${{ + # github.event_name == 'push' && + # github.ref == 'refs/heads/main' + # }} + VALIDATE_ALL_CODEBASE: true + + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # ADD YOUR CUSTOM ENV VARIABLES HERE TO OVERRIDE VALUES OF + # .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY + + # Upload MegaLinter artifacts + - name: Archive production artifacts + uses: actions/upload-artifact@v3 + if: success() || failure() + with: + name: MegaLinter reports + path: | + megalinter-reports + mega-linter.log + + # Create pull request if applicable + # (for now works only on PR from same repository, not from forks) + - name: Create Pull Request with applied fixes + uses: peter-evans/create-pull-request@v5 + id: cpr + if: >- + steps.ml.outputs.has_updated_sources == 1 && + ( + env.APPLY_FIXES_EVENT == 'all' || + env.APPLY_FIXES_EVENT == github.event_name + ) && + env.APPLY_FIXES_MODE == 'pull_request' && + ( + github.event_name == 'push' || + github.event.pull_request.head.repo.full_name == github.repository + ) && + !contains(github.event.head_commit.message, 'skip fix') + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + commit-message: "[MegaLinter] Apply linters automatic fixes" + title: "[MegaLinter] Apply linters automatic fixes" + labels: bot + + - name: Create PR output + if: >- + steps.ml.outputs.has_updated_sources == 1 && + ( + env.APPLY_FIXES_EVENT == 'all' || + env.APPLY_FIXES_EVENT == github.event_name + ) && + env.APPLY_FIXES_MODE == 'pull_request' && + ( + github.event_name == 'push' || + github.event.pull_request.head.repo.full_name == github.repository + ) && + !contains(github.event.head_commit.message, 'skip fix') + run: | + echo "PR Number - ${{ steps.cpr.outputs.pull-request-number }}" + echo "PR URL - ${{ steps.cpr.outputs.pull-request-url }}" + + # Push new commit if applicable + # (for now works only on PR from same repository, not from forks) + - name: Prepare commit + if: >- + steps.ml.outputs.has_updated_sources == 1 && + ( + env.APPLY_FIXES_EVENT == 'all' || + env.APPLY_FIXES_EVENT == github.event_name + ) && + env.APPLY_FIXES_MODE == 'commit' && + github.ref != 'refs/heads/main' && + ( + github.event_name == 'push' || + github.event.pull_request.head.repo.full_name == github.repository + ) && + !contains(github.event.head_commit.message, 'skip fix') + run: sudo chown -Rc $UID .git/ + + - name: Commit and push applied linter fixes + uses: stefanzweifel/git-auto-commit-action@v4 + if: >- + steps.ml.outputs.has_updated_sources == 1 && + ( + env.APPLY_FIXES_EVENT == 'all' || + env.APPLY_FIXES_EVENT == github.event_name + ) && + env.APPLY_FIXES_MODE == 'commit' && + github.ref != 'refs/heads/main' && + ( + github.event_name == 'push' || + github.event.pull_request.head.repo.full_name == github.repository + ) && + !contains(github.event.head_commit.message, 'skip fix') + with: + branch: >- + ${{ + github.event.pull_request.head.ref || + github.head_ref || + github.ref + }} + commit_message: "[MegaLinter] Apply linters fixes" + commit_user_name: megalinter-bot + commit_user_email: nicolas.vuillamy@ox.security diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 0000000..bf6aa2d --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,33 @@ +--- +# This workflow will install Python dependencies and run tests with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python tests + +on: + push: + # Run against all branches + # branches: [ "main" ] + pull_request: + branches: ["main"] + +permissions: + contents: read + +jobs: + # Inspired by https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#caching-packages + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install poetry + run: pipx install poetry + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.11" + cache: "poetry" + - name: Install dependencies + run: poetry install + - name: Run tests + run: poetry run pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a105c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +settings.yaml +*.pkl +__pycache__ +.vscode +build +dist +*.egg-info +.venv +megalinter-reports/ diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..2bb1f85 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,6 @@ +[allowlist] +# Plejd API key is not a secret, but detected so by gitleaks +description = "Plejd API key" +regexes = [ +'''zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak''' +] diff --git a/.hook-scripts/clean-up-pyc-and-pyo-files b/.hook-scripts/clean-up-pyc-and-pyo-files new file mode 100755 index 0000000..8440194 --- /dev/null +++ b/.hook-scripts/clean-up-pyc-and-pyo-files @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +find . -iname '*.pyc' -delete +find . -iname '*.pyo' -delete diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 0000000..2cee5f5 --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,15 @@ +{ + "threshold": 0, + "reporters": ["html", "markdown"], + "ignore": [ + "**/node_modules/**", + "**/.git/**", + "**/.rbenv/**", + "**/.venv/**", + "**/*cache*/**", + "**/.github/**", + "**/.idea/**", + "**/report/**", + "**/*.svg" + ] +} diff --git a/.mega-linter.yml b/.mega-linter.yml new file mode 100644 index 0000000..b3428b6 --- /dev/null +++ b/.mega-linter.yml @@ -0,0 +1,35 @@ +# Configuration file for MegaLinter +# +# See all available variables at https://megalinter.io/latest/config-file/ and in +# linters documentation + +# all, none, or list of linter keys +APPLY_FIXES: all + +PYTHON_PYLINT_PRE_COMMANDS: + # Help pylint understand pydantic models + - command: pip install pylint-pydantic + venv: pylint + continue_if_failed: false + +PYTHON_PYLINT_ARGUMENTS: + # Disable import checks because it needs all the Python dependencies installed in the linter + # https://github.com/oxsecurity/megalinter/issues/2030 + - "--disable=E0401,E0611" + # Help pylint understand pydantic models + - "--load-plugins=pylint_pydantic" +PYTHON_FLAKE8_ARGUMENTS: + - "--max-line-length=100" + +DISABLE_LINTERS: + - SPELL_CSPELL + - PYTHON_PYRIGHT # TODO this should not be disabled!! + - PYTHON_MYPY # TODO this should not be disabled!! + - REPOSITORY_CHECKOV # TODO this should not be disabled!! + - COPYPASTE_JSCPD # TODO this should not be disabled!! + +SHOW_ELAPSED_TIME: true + +FILEIO_REPORTER: false + +PYTHON_BANDIT_FILTER_REGEX_EXCLUDE: test_ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,46 @@ +--- +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: local + hooks: + - id: clean-up-pyc-and-pyo-files + name: Scrub all .pyc and .pyo files before committing + entry: ./.hook-scripts/clean-up-pyc-and-pyo-files + language: script + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/psf/black + rev: "23.1.0" + hooks: + - id: black + args: [--line-length=100] + + - repo: https://github.com/pycqa/flake8 + rev: "6.0.0" + hooks: + - id: flake8 + args: [--max-line-length=100] + + - repo: https://github.com/thlorenz/doctoc + rev: v2.2.0 + hooks: + - id: doctoc + + - repo: https://github.com/python-poetry/poetry + rev: "1.4.1" + hooks: + - id: poetry-check + - id: poetry-lock diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f37e938 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,92 @@ +# Contributing to Plejd plejd-mqtt-ha + +Thanks for considering contributing to the project! + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Getting Started](#getting-started) + - [Setting up the environment](#setting-up-the-environment) +- [Coding Standards](#coding-standards) +- [Pull Requests](#pull-requests) + - [Code linting](#code-linting) +- [Pre-commit Hooks](#pre-commit-hooks) + + + +## Getting Started + +Before you start contributing, please review these guidelines: + +1. Check out the project's issue tracker and pull requests to see if someone else has already reported and/or fixed the issue you're facing. + +2. If not, open a new issue. Please provide as much information as possible to help the maintainers understand and solve the problem. + +3. If you think you can fix or implement it yourself, fork the project and submit a pull request. Please make sure to follow the coding standards and test your changes. + +### Setting up the environment + +This project uses [Poetry](https://python-poetry.org/) for dependency management. If you don't have Poetry installed, you can install it with: + +```bash +curl -sSL https://install.python-poetry.org | python - +``` + +Once Poetry is installed, you can set up your environment with: + +```bash +poetry install +``` +This will create a virtual environment and install all the necessary dependencies so you can start contributing to the project. +To spawn a shell within the environment: + +```bash +poetry shell +``` + +## Coding Standards + +Please ensure your code adheres to the following standards: + +- Follow the style used in the existing codebase. +- Include comments in your code where necessary. +- Write tests for your changes. + +## Pull Requests + +When submitting a pull request: + +- Include a description of what your change intends to do. +- Be sure to link to the issue that your change is related to, if applicable. +- Make sure your pull request includes tests. + +### Code linting +This project uses [Mega-Linter](https://nvuillam.github.io/mega-linter/), an open-source linter that analyzes consistency and quality of your code. + +Before submitting a pull request, please ensure your changes do not introduce any new linting errors. You can run Mega-Linter locally to check your code before committing: + +```bash +npx mega-linter-runner --flavor python +``` +This command will run Mega-Linter against your local codebase and report any issues it finds. + +## Pre-commit Hooks + +This project uses [pre-commit](https://pre-commit.com/) to ensure that code committed to the repository meets certain standards and passes linting tests. Before you can commit changes to the repository, your changes will be automatically checked by pre-commit hooks. + +To install the pre-commit hooks, you need to have pre-commit installed on your local machine. You can install it using pip: + +```bash +pip install pre-commit +``` + +Once pre-commit is installed, you can install the pre-commit hooks with: + +```bash +pre-commit install +``` + +Now, the hooks will automatically run every time you commit changes to the repository. If the hooks find issues, your commit will be blocked until you fix the issues. + +Looking to your contributions. Thank you! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6b83906 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +FROM python:3.11 as base + +ENV HEALTHCHECK_INTERVAL=5m \ + HEALTHCHECK_TIMEOUT=5s \ + SETTINGS_DIR=~/.plejd/ \ + SETTINGS_FILE=settings.yaml \ + WORKDIR=/app + +RUN apt-get update \ + && apt-get --no-install-recommends install -y bluez=5.66-1 bluetooth=5.66-1 \ + && rm -rf /var/lib/apt/lists/* \ + && adduser --disabled-password --gecos '' plejd + +WORKDIR $WORKDIR +RUN chown plejd:plejd $WORKDIR + +# Start building stage +FROM base as builder + +USER plejd + +RUN pip install --no-cache-dir --user poetry==1.7.1 + +# Ensure the poetry command is available +ENV PATH="/home/plejd/.local/bin:${POETRY_HOME}/bin:${PATH}" + +# Copy poetry files +COPY poetry.lock pyproject.toml README.md $WORKDIR/ + +# Copy application files +COPY ./plejd_mqtt_ha $WORKDIR/plejd_mqtt_ha + +# Install and build dependencies using poetry +RUN poetry config virtualenvs.in-project true && \ + poetry install --only=main --no-root && \ + poetry build + +# Start final stage +FROM base as final + +ENV PYTHONPATH="$WORKDIR/.venv/lib/python3.11/site-packages:${PYTHONPATH}" + +# Copy the built virtualenv deps from the builder stage +COPY --from=builder /app/.venv $WORKDIR/.venv +COPY --from=builder /app/dist $WORKDIR/ +COPY docker-entrypoint.sh $WORKDIR/ + +RUN ./.venv/bin/pip install "$WORKDIR"/*.whl + +# Healthcheck +COPY healthcheck.py $WORKDIR/healthcheck.py +HEALTHCHECK --interval=5m --timeout=1s \ + CMD python $WORKDIR/healthcheck.py || exit 1 + +# Copy settings file +COPY $SETTINGS_DIR/$SETTINGS_FILE $WORKDIR/$SETTINGS_FILE + +# Set the entrypoint +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed515e2 --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# plejd-mqtt-ha +A simple mqtt gateway to connect your plejd devices to an MQTT broker and Home Assistant + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Description](#description) +- [Getting started](#getting-started) + - [Setting Up and Running the Program Locally](#setting-up-and-running-the-program-locally) + - [Prerequisites](#prerequisites) + - [Installing Python Dependencies](#installing-python-dependencies) + - [Activating the Virtual Environment](#activating-the-virtual-environment) + - [Running the program](#running-the-program) + - [Setting Up and Running as a Docker Container](#setting-up-and-running-as-a-docker-container) + - [Building the Docker Image](#building-the-docker-image) + - [Running the Docker Container](#running-the-docker-container) +- [Configuration](#configuration) + - [General](#general) + - [API](#api) + - [MQTT](#mqtt) + - [BLE](#ble) + - [Example](#example) +- [Acknowledgements](#acknowledgements) +- [License](#license) +- [Contributing](#contributing) + + + +## Description +This project connects bluetooth devices in a plejd mesh to an MQTT broker. Devices are discovered automatically by HomeAssistant. + +The project currently supports: +- Plejd Light + +Not supported currently: +- Plejd Switch +- Plejd Device Trigger +- Plejd Sensor +- Plejd scenes + +## Getting started + +This section provides step-by-step instructions on how to set up and run the program on your local machine. + +The project is intended to run as a docker container, but can also be run locally. + +### Setting Up and Running the Program Locally + +This section provides step-by-step instructions on how to set up and run the program on your local machine. + +#### Prerequisites + +Ensure that you have the following dependencies installed on your system: + +- `python`: The programming language used for this project. +- `poetry`: A tool for dependency management in Python. +- `bluetooth`: A library to work with Bluetooth. +- `bluez`: Official Linux Bluetooth protocol stack. + +Refer to the Dockerfile in the project root for specific versions of these dependencies. + +#### Installing Python Dependencies + +After installing the prerequisites, you can install the Python dependencies for the project. Run the following command in your terminal: + +```bash +poetry install +``` +#### Activating the Virtual Environment +Next, spawn a new virtual shell using the following command: +Start the program using: + +```bash +poetry shell +``` + +This command activates the virtual environment, isolating your project dependencies from other Python projects. +#### Running the program + +Finally, you can start the program using the following command: + +`python -m plejd_mqtt_ha` + +This command runs the plejd_mqtt_ha module as a script, starting the program. + +### Setting Up and Running as a Docker Container + +This section provides instructions on how to build and run the program as a Docker container. Docker must be installed on your host machine to follow these steps. + +#### Building the Docker Image + +First, build a Docker image for the program. This creates a reusable image that contains the program and all its dependencies. Run the following command in your terminal: + +```bash +docker build . -t plejd +``` +This command builds a Docker image from the Dockerfile in the current directory, and tags the image with the name `plejd` + +#### Running the Docker Container + +After building the image, you can create and start a Docker container from it. Run the following command in your terminal: + +```bash +docker run plejd +``` + +This command starts a new Docker container from the plejd image. The program inside the container will start running immediately. + +## Configuration + +Configuration of the application. See example configuration [here](#example). + +### General + +| Parameter | Description | Default | +|------------------------------------------|--------------------------------------------------------|-------------| +| `health_check` (Optional) | Enable health check | True | +| `health_check_interval` (Optional) | Interval in seconds between writing health check files | 60.0 | +| `health_check_dir` (Optional) | Directory to store health check files | "~/.plejd/" | +| `health_check_bt_file` (Optional) | File name for Bluetooth health check | "bluetooth" | +| `health_check_mqtt_file` (Optional) | File name for MQTT health check | "mqtt" | +| `health_check_heartbeat_file` (Optional) | File name for heartbeat file | "heartbeat" | + +### API + +Settings related to Plejd API. + +| Parameter | Description | Default | +|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------| +| `user` | Plejd user name (email) | | +| `password` | Password of the Plejd user | | +| `site` (Optional) | Name of the Plejd site to use | First in the accounts list | +| `timeout` (Optional) | Timeout to reach Plejd API | 10.0 | +| `cache_policy` (Optional) | Cache policy to use. Can be one of the following: | `FIRST_CACHE` | +| `cache_dir` (Optional) | Directory to store cached site. Not used if `cache_policy` is set to `NO_CACHE`. | `~/.plejd/` | +| `cache_file` (Optional) | File name for cached site. Not used if `cache_policy` is set to `NO_CACHE`. | `site.json` | + +### MQTT + +Settings related to MQTT broker. + +| Parameter | Description | Default | +|----------------------------------|---------------------------------|-----------------| +| `host` (Optional) | Address of the host MQTT broker | "localhost" | +| `port` (Optional) | Port of the MQTT host | 1883 | +| `user` (Optional) | MQTT user name | None | +| `password` (Optional) | Password of the MQTT user | None | +| `ha_discovery_prefix` (Optional) | Home assistant discovery prefix | "homeassistant" | + +### BLE + +Settings related to BLE. + +| Parameter | Description | Default | +|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| +| `adapter` (Optional) | If a specific Bluetooth adapter is to be used. **NOT USED YET!** | None | +| `scan_time` (Optional) | Time to scan for Plejd Bluetooth devices | 10.0 | +| `retries` (Optional) | Number of times to try and reconnect to Plejd mesh | 10 | +| `time_retries` (Optional) | Time between retries | 10.0 | +| `preferred_device` (Optional) | If a specific Plejd device is to be used as mesh ingress point. If not set, the device with the strongest signal will be used. Not recommended to use this setting. **NOT USED YET!** | None | + +### Example + +Here is an example configuration with some common settings: + +```yaml +api: + user: "your-email@example.com" + password: "your-password" + +mqtt: + host: "mqtt.example.com" + port: 1883 + user: "mqtt-user" + password: "mqtt-password" + +ble: + scan_time: 20.0 +``` + +## Acknowledgements + +This project is inspired by the following repositories: + +- [ha-plejd](https://github.com/klali/ha-plejd) +- [hassio-plejd](https://github.com/icanos/hassio-plejd) + +It also utilizes the [ha-mqtt-discoverable](https://github.com/unixorn/ha-mqtt-discoverable) library +for creating MQTT devices automatically discovered by Home Assistant. + +A special thanks to the authors and contributors of these projects for their work and inspiration. + +## License + +This project is open source and available under the [Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). + +## Contributing + +Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for details on how to contribute to this project. diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..51b3575 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Inspired by +# https://medium.com/omi-uulm/how-to-run-containerized-bluetooth-applications-with-bluez-dced9ab767f6 + +sudo service dbus start +sudo service bluetooth start + +# We must wait for bluetooth service to start +msg="Waiting for services to start..." +time=0 +echo -n "$msg" +while [[ "$(pidof start-stop-daemon)" != "" ]]; do + sleep 1 + time=$((time + 1)) + echo -en "\r$msg $time s" +done +echo -e "\r$msg done! (in $time s)" + +# Reset adapter in case it was stuck from previous session +hciconfig hci0 down +hciconfig hci0 up + +echo -e "Starting plejd" +python -m plejd_mqtt_ha +tail -f /dev/null diff --git a/healthcheck.py b/healthcheck.py new file mode 100644 index 0000000..8d289f6 --- /dev/null +++ b/healthcheck.py @@ -0,0 +1,201 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Healthcheck program used to check if plejd-mqtt is running.""" + +import argparse +import logging +import sys +import time + +import yaml +from plejd_mqtt_ha import constants +from plejd_mqtt_ha.mdl.settings import PlejdSettings +from pydantic import ValidationError + +STATUS_ERR = 1 +STATUS_OK = 0 + + +def is_program_running(plejd_settings: PlejdSettings) -> bool: + """Check heartbeat and that program is running. + + Parameters + ---------- + plejd_settings : PlejdSettings + Settings to use for healthcheck + + Returns + ------- + bool + True if program is running, False otherwise + """ + try: + # Open the file + heartbeat_file = plejd_settings.health_check_dir + plejd_settings.health_check_hearbeat_file + with open(heartbeat_file, "r") as f: + # Read the file + contents = f.read() + + # Convert the contents to a float + last_heartbeat = float(contents) + + # Get the current time + current_time = time.time() + + # Check if the heartbeat is recent enough + if current_time - last_heartbeat < plejd_settings.health_check_interval: + return True + else: + return False + except (FileNotFoundError, IOError, ValueError) as e: + logging.error(f"Error during healthcheck: {e}") + return False + + +def is_blueooth_connected(plejd_settings: PlejdSettings) -> bool: + """Check if bluetooth is connected. + + Parameters + ---------- + plejd_settings : PlejdSettings + Settings to use for healthcheck + + Returns + ------- + bool + True if bluetooth is connected, False otherwise + """ + try: + # Open the file + bt_status_file = plejd_settings.health_check_dir + plejd_settings.health_check_bt_file + with open(bt_status_file, "r") as f: + # Read the file + contents = f.read() + + # Check if the first word is "connected" + if contents.split()[0] == "connected": + return True + else: + return False + except (FileNotFoundError, IOError, ValueError) as e: + logging.error(f"Error during healthcheck: {e}") + return False + + +def is_mqtt_connected(plejd_settings: PlejdSettings) -> bool: + """Check if MQTT is connected. + + Parameters + ---------- + plejd_settings : PlejdSettings + Settings to use for healthcheck + + Returns + ------- + bool + True if MQTT is connected, False otherwise + """ + try: + # Open the file + mqtt_status_file = plejd_settings.health_check_dir + plejd_settings.health_check_bt_file + with open(mqtt_status_file, "r") as f: + # Read the file + contents = f.read() + + # Check if the first word is "connected" + if contents.split()[0] == "connected": + return True + else: + return False + except (FileNotFoundError, IOError, ValueError) as e: + logging.error(f"Error during healthcheck: {e}") + return False + + +def healthcheck(plejd_settings: PlejdSettings) -> int: + """Perform healthcheck. + + Parameters + ---------- + plejd_settings : PlejdSettings + Settings to use for healthcheck + + Returns + ------- + int + STATUS_OK if healthcheck succeeds, STATUS_ERR if it fails + """ + if not is_program_running(plejd_settings): + logging.error("Plejd program is not running") + return STATUS_ERR + if not is_blueooth_connected(plejd_settings): + logging.error("Bluetooth is not connected") + return STATUS_ERR + if not is_mqtt_connected(plejd_settings): + logging.error("MQTT is not connected") + return STATUS_ERR + + logging.info("All healthcheck passed") + return STATUS_OK # All checks passed, return OK + + +def main() -> int: + """Entry point for the application script. + + Returns + ------- + int + Returns STATUS_OK if healthcheck succeeds, STATUS_ERR if it fails + + Raises + ------ + ValueError + If invalid log level is provided + """ + parser = argparse.ArgumentParser(description="Healthcheck program") + parser.add_argument("--loglevel", type=str, help="Set log level", default="ERROR") + args = parser.parse_args() + + numeric_level = getattr(logging, args.loglevel.upper(), None) + if not isinstance(numeric_level, int): + raise ValueError(f"Invalid log level: {args.loglevel}") + + logging.basicConfig(level=numeric_level, format="%(asctime)s %(levelname)s %(message)s") + + logging.info("Starting healthcheck") + + logging.info("Loading settings") + + try: + with open(constants.settings_file, "r") as file: + settings_yaml = yaml.safe_load(file) + + plejd_settings = PlejdSettings.parse_obj(settings_yaml) + except FileNotFoundError: + logging.critical("The settings.yaml file was not found.") + return STATUS_ERR + except yaml.YAMLError: + logging.critical("There was an error parsing the settings.yaml file.") + return STATUS_ERR + except ValidationError as e: + logging.critical( + f"There was an error parsing the settings into a PlejdSettings object: {e}" + ) + return STATUS_ERR + + return healthcheck(plejd_settings) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plejd_mqtt_ha/__init__.py b/plejd_mqtt_ha/__init__.py new file mode 100644 index 0000000..b6ee697 --- /dev/null +++ b/plejd_mqtt_ha/__init__.py @@ -0,0 +1 @@ +"""The plejd_mqtt_ha module.""" diff --git a/plejd_mqtt_ha/__main__.py b/plejd_mqtt_ha/__main__.py new file mode 100644 index 0000000..faa4732 --- /dev/null +++ b/plejd_mqtt_ha/__main__.py @@ -0,0 +1,28 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Main module.""" + +import sys + +from plejd_mqtt_ha import plejd + + +def main(): + """Entry point of the application.""" + sys.exit(plejd.start()) + + +if __name__ == "__main__": + main() diff --git a/plejd_mqtt_ha/bt_client.py b/plejd_mqtt_ha/bt_client.py new file mode 100644 index 0000000..1af6e87 --- /dev/null +++ b/plejd_mqtt_ha/bt_client.py @@ -0,0 +1,569 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Bluetooth client for Plejd mesh. + +This module handles the BLE communication with the Plejd mesh. It uses the bleak library to +communicate with the Plejd mesh. +""" + +import asyncio +import hashlib +import logging +import struct +from datetime import datetime +from random import randbytes +from typing import Any, Callable, Optional + +import bleak +import numpy as np +from bleak import BleakClient +from bleak.exc import BleakDBusError, BleakError +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from plejd_mqtt_ha import constants +from plejd_mqtt_ha.mdl.bt_device_info import BTDeviceInfo +from plejd_mqtt_ha.mdl.settings import PlejdSettings + + +class PlejdBluetoothError(Exception): + """Base class for exceptions in this module.""" + + def __init__(self, message: str): + """Initialize exception. + + Parameters + ---------- + message : str + Error message + """ + self.message = message + super().__init__(self.message) + + +class PlejdNotConnectedError(PlejdBluetoothError): + """Exception raised for errors if using a Plejd Bluetooth connection when not connected.""" + + pass + + +class PlejdTimeoutError(PlejdBluetoothError): + """Exception raised for errors when a timeout occurs in Plejd Bluetooth connection.""" + + pass + + +class UnsupportedCharacteristicError(PlejdBluetoothError): + """Exception raised for errors when trying to use an unsupported characteristic.""" + + pass + + +class UnsupportedCommandError(PlejdBluetoothError): + """Exception raised for errors when trying to use an unknown command.""" + + pass + + +class BTClient: + """ + Plejd bluetooth handler. Connect and interact with the physical devices in your plejd mesh. + + BLE connectivity is handled by SimpleBLE library and its python bindings SimplePYBLE + """ + + def __init__(self, crypto_key: str, settings: PlejdSettings): + """Initialize the BTClient instance. + + Parameters + ---------- + crypto_key : str + Crypto key to use for encryption/decryption of data to/from Plejd mesh + settings : PlejdSettings + Settings for the Plejd mesh + """ + self._client: Optional[BleakClient] = None + self._disconnect = False + self._settings = settings + self._crypto_key = crypto_key + self._callbacks: dict = {} + self._crypto_key = self._crypto_key.replace("-", "") + self._crypto_key = bytes.fromhex(self._crypto_key) + + async def _stay_connected_loop(self) -> None: + async def heartbeat(): + while not self._disconnect: + retries = 0 + + while retries < self._settings.ble.retries: + await asyncio.sleep(self._settings.ble.time_retries) + + if not self.is_connected(): + await self._connect() + retries += 1 + continue + else: + if not await self.ping(): + retries += 1 + continue + + if retries >= self._settings.ble.retries: + logging.warning("Hearbeat timeout") + + await heartbeat() + + async def connect(self, stay_connected: bool) -> bool: + """Connect to plejd mesh. + + Parameters + ---------- + stay_connected : bool + If True, makes sure user stays conneted as a background task + + Returns + ------- + bool + True if connection was successful + """ + logging.info("Connecting to plejd mesh") + if not self.is_connected(): + if not await self._connect(): + return False + + logging.info("Connected to plejd mesh") + if stay_connected: + event_loop = asyncio.get_event_loop() + event_loop.create_task(self._stay_connected_loop()) + + return True + + async def disconnect(self) -> bool: + """Disconnect from plejd mesh. + + Returns + ------- + bool + True if disconnect was successful + """ + self._disconnect = True + if self._client and self.is_connected(): + return await self._client.disconnect() + + return True + + def is_connected(self) -> bool: + """Check if connected to plejd mesh. + + Returns + ------- + bool + True if connected to Plejd mesh + """ + return self._client is not None and self._client.is_connected + + async def send_command( + self, + ble_address: int, + command: int, + data: str, + response_type: int, + ) -> None: + """Send command to Plejd BLE device. + + Parameters + ---------- + ble_address : int + Address of the device to send command to + command : int + Command to send + data : str + Data to send + response_type : int + Response type + """ + if not self.is_connected(): + error_message = "Trying to send command when not connected to Plejd mesh" + logging.error(error_message) + raise PlejdNotConnectedError(error_message) + + try: + constants.PlejdCommand(command) + except ValueError as err: + error_message = f"Trying to send unknown command {command}" + logging.error(error_message) + raise UnsupportedCommandError(error_message) from err + + payload = self._get_cmd_payload(ble_address, command, data, response_type) + encoded_address = self._encode_address(self._client.address) + encoded_data = self._encrypt_decrypt_data(self._crypto_key, encoded_address, payload) + + try: + await self._write_request(constants.PlejdCharacteristic.DATA_UUID.value, encoded_data) + except (PlejdBluetoothError, PlejdNotConnectedError, PlejdTimeoutError) as err: + logging.error(f"Caught an exception when calling _write_request: {str(err)}") + raise + + async def subscribe_last_data( + self, ble_address: int, callback: Callable[[bytearray], Any] + ) -> None: + """Subscribe to last data received from the mesh. + + Parameters + ---------- + ble_address : int + Address of the device to listen to + callback : Callable[[bytearray],Any] + Callback function that shall be invoked on data received by the mesh + """ + if not self.is_connected(): + error_message = "Trying to subscribe to last data when not connected to Plejd mesh" + logging.error(error_message) + raise PlejdNotConnectedError(error_message) + + self._callbacks.update({ble_address: callback}) + + def _proxy_callback(_, data: bytearray) -> None: + if not self._client: + return + encoded_address = self._encode_address(self._client.address) + decrypted_data = self._encrypt_decrypt_data(self._crypto_key, encoded_address, data) + # Since it is a mesh one client handles all subscriptions and we need to dispatch to + # the correct device + sender_addr = decrypted_data[0] # Address of the device that sent data in the mesh + if sender_addr not in self._callbacks: + logging.warning( + f"Received data from device address {sender_addr} but found no registered" + "callback" + ) + return + self._callbacks[sender_addr](bytearray(decrypted_data)) # Sender callback + + await self._client.start_notify( + constants.PlejdCharacteristic.LAST_DATA_UUID.value, _proxy_callback + ) + + async def ping(self) -> bool: + """Ping plejd mesh to check if it is online. + + Returns + ------- + bool + True if ping was successful + """ + ping_data = randbytes(1) # any arbitrary payload can be used + + try: + await self._write_request(constants.PlejdCharacteristic.PING_UUID.value, ping_data) + pong_data = await self._read_request(constants.PlejdCharacteristic.PING_UUID.value) + except (PlejdBluetoothError, PlejdNotConnectedError, PlejdTimeoutError) as err: + logging.info(f"Ping operation failed: {str(err)}") + return False + + if pong_data == bytearray() or not ((ping_data[0] + 1) & 0xFF) == pong_data[0]: + return False + + return True + + async def get_last_data(self) -> bytes: + """ + Retrieve last data from plejd mesh. Can for example be used to extract plejd time. + + Returns + ------- + bytes + Last data payload + """ + if not self.is_connected(): + error_message = "Trying to get last data when not connected to Plejd mesh" + logging.error(error_message) + raise PlejdNotConnectedError(error_message) + + try: + encrypted_data = await self._read_request(constants.PlejdCharacteristic.DATA_UUID.value) + except (PlejdBluetoothError, PlejdNotConnectedError, PlejdTimeoutError) as err: + logging.error(f"Caught an exception when calling _read_request: {str(err)}") + raise + + encoded_address = self._encode_address(self._client.address) + return self._encrypt_decrypt_data(self._crypto_key, encoded_address, encrypted_data) + + async def get_plejd_time(self, plejd_device: BTDeviceInfo) -> Optional[datetime]: + """Request time from plejd mesh. + + Parameters + ---------- + plejd_device : PlejdDevice + Plejd device to get time from, can be any device in the mesh. Does not really matter + which one. + + Returns + ------- + Optional[datetime] + Returns the time in datetime format + """ + if not self.is_connected(): + error_message = "Trying to get time when not connected to Plejd mesh" + logging.error(error_message) + raise PlejdNotConnectedError(error_message) + + try: + # Request time + await self.send_command( + plejd_device.ble_address, + constants.PlejdCommand.BLE_CMD_TIME_UPDATE.value, + "", + constants.PlejdResponse.BLE_REQUEST_RESPONSE.value, + ) + # Read respone + last_data = await self.get_last_data() + except (PlejdBluetoothError, PlejdNotConnectedError, PlejdTimeoutError): + logging.error("Failed to read time from Plejd mesh, when calling get_last_data") + raise + + # Make sure we receive the time update command + if not last_data or not ( + int.from_bytes(last_data[3:5], "big") + == constants.PlejdCommand.BLE_CMD_TIME_UPDATE.value + ): + logging.warning( + "Failed to read time from Plejd mesh, using device: %s", + plejd_device.name, + ) + raise UnsupportedCommandError("Received unknown command") + + # Convert from unix timestamp + plejd_time = datetime.fromtimestamp(struct.unpack_from(" bool: + """Set time in plejd mesh. + + Parameters + ---------- + plejd_device : PlejdDevice + Plejd device to use to set time, can be any device in the mesh. Does not really matter + which one. + time : datetime + Time to set in datetime format + + Returns + ------- + bool + Boolean status of the operation + """ + timestamp = struct.pack(" bool: + self._disconnect = False + + # Scan for Plejd devices + try: + scanner = bleak.BleakScanner(service_uuids=[constants.PLEJD_SERVICE]) + await scanner.start() + await asyncio.sleep(self._settings.ble.scan_time) + await scanner.stop() + except (BleakError, BleakDBusError) as err: + logging.warning(f"Could not start BLE scanner: {str(err)}") + return False + except Exception as err: + logging.warning(f"Unknown error when starting BLE scanner: {str(err)}") + return False + + logging.debug( + f"Successfully started BLE scanner, found {len(scanner.discovered_devices)} devices" + ) + + if not scanner.discovered_devices: + logging.warning("Could not find any plejd devices") + return False + + # Find device with strongest signal + curr_rssi = -255 + plejd_device = None + for [ + device, + advertisement, + ] in scanner.discovered_devices_and_advertisement_data.values(): + if advertisement.rssi > curr_rssi: + curr_rssi = advertisement.rssi + plejd_device = device + + if not plejd_device: + logging.warning("Could not find any plejd devices") + return False + + logging.debug(f"Using device {plejd_device} with signal strenth {curr_rssi}") + + # Connect to plejd mesh using the selected device + self._client = BleakClient(plejd_device) + if not await self._client.connect(): + logging.warning("Could not connect to plejd device") + return False + + # Authenticate to plejd mesh + if not await self._auth_challenge_response(): + logging.warning("Could not authenticate plejd mesh") + return False + + return True + + async def _auth_challenge_response(self) -> bool: + # Initiate challenge + encoded_data = b"\x00" + try: + await self._write_request(constants.PlejdCharacteristic.AUTH_UUID.value, encoded_data) + except (PlejdBluetoothError, PlejdNotConnectedError, PlejdTimeoutError) as err: + logging.warning(f"Failed to initiate auth challenge: {str(err)}") + return False + + # Read challenge + try: + challenge = await self._read_request(constants.PlejdCharacteristic.AUTH_UUID.value) + + key_int = int.from_bytes(self._crypto_key, "big") + challenge_int = int.from_bytes(challenge, "big") + + intermediate = hashlib.sha256((key_int ^ challenge_int).to_bytes(16, "big")).digest() + part1 = int.from_bytes(intermediate[:16], "big") + part2 = int.from_bytes(intermediate[16:], "big") + response = (part1 ^ part2).to_bytes(16, "big") + await self._write_request(constants.PlejdCharacteristic.AUTH_UUID.value, response) + except (PlejdBluetoothError, PlejdNotConnectedError, PlejdTimeoutError) as err: + logging.warning(f"Failed to perform challenge response: {str(err)}") + return False + + return True + + async def _write_request(self, characteristic_uuid, data) -> None: + if not self.is_connected(): + error_message = ( + f"Trying to write request to characteristic {characteristic_uuid} " + " when not connected to Plejd mesh" + ) + logging.error(error_message) + raise PlejdNotConnectedError(error_message) + + try: + constants.PlejdCharacteristic(characteristic_uuid) + except ValueError as err: + error_message = ( + f"Trying to write request to characteristic {characteristic_uuid} " + "that is not a valid PlejdCharacteristic" + ) + logging.error(error_message) + raise UnsupportedCharacteristicError(error_message) from err + + try: + await self._client.write_gatt_char(characteristic_uuid, data) + except (BleakError, BleakDBusError) as err: + error_message = f"Failed to write to characteristic {characteristic_uuid}: {str(err)}" + logging.error(error_message) + raise PlejdBluetoothError(error_message) from err + except asyncio.TimeoutError as err: + error_message = ( + "Timeout error when writing to characteristic " f"{characteristic_uuid}: {str(err)}" + ) + logging.error(error_message) + raise PlejdTimeoutError(error_message) from err + + async def _read_request(self, characteristic_uuid) -> bytearray: + if not self.is_connected(): + error_message = ( + f"Trying to read request from characteristic {characteristic_uuid} " + " when not connected to Plejd mesh" + ) + logging.error(error_message) + raise PlejdNotConnectedError(error_message) + + try: + constants.PlejdCharacteristic(characteristic_uuid) + data = await self._client.read_gatt_char(characteristic_uuid) + except ValueError as err: + error_message = ( + f"Trying to read request from characteristic {characteristic_uuid} " + "that is not a valid PlejdCharacteristic" + ) + logging.error(error_message) + raise UnsupportedCharacteristicError(error_message) from err + except (BleakError, BleakDBusError) as err: + error_message = f"Failed to read from characteristic {characteristic_uuid}: {str(err)}" + logging.error(error_message) + raise PlejdBluetoothError(error_message) from err + except asyncio.TimeoutError as err: + error_message = ( + "Timeout error when reading from characteristic" + f"{characteristic_uuid}: {str(err)}" + ) + logging.error(error_message) + raise PlejdTimeoutError(error_message) from err + + return data + + def _encrypt_decrypt_data(self, key: bytes, addr: str, data: bytearray) -> bytes: + buf = bytearray(addr * 2) + buf += addr[:4] + + # The API requires the use of ECB mode for encryption. This is generally considered + # insecure, but it's necessary for compatibility with the API. Bandit will complain about + # this, but we can ignore it. + cipher = ( + Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()) # nosec + .encryptor() + .update(buf) + ) + + output = b"" + for i, byte in enumerate(data): + output += struct.pack("B", byte ^ cipher[i % 16]) + + return output + + def _encode_address(self, addr: str) -> bytes: + ret = bytes.fromhex(addr.replace(":", "")) + return ret[::-1] + + def _get_cmd_payload( + self, ble_address: int, command: int, hex_str: str, response_type: int + ) -> bytearray: + buffer_length = 5 + payload = bytearray(buffer_length) + + struct.pack_into( + ">BHH", + payload, + 0, + np.uint8(ble_address), + np.ushort(response_type), + np.ushort(command), + ) + + hex_data_bytes = bytearray.fromhex(hex_str) + payload.extend(hex_data_bytes) + + return payload diff --git a/plejd_mqtt_ha/constants.py b/plejd_mqtt_ha/constants.py new file mode 100644 index 0000000..f62e73c --- /dev/null +++ b/plejd_mqtt_ha/constants.py @@ -0,0 +1,106 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Project wide constants.""" + +#################################################################################################### +# Plejd API +#################################################################################################### + +import os +from enum import Enum + +API_APP_ID = "zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak" +API_BASE_URL = "https://cloud.plejd.com/parse/" +API_LOGIN_URL = "login" +API_SITE_LIST_URL = "functions/getSiteList" +API_SITE_DETAILS_URL = "functions/getSiteById" +API_DEFAULT_SITE_NAME = None + +#################################################################################################### +# Plejd BLE +#################################################################################################### + +# Plejd service +BLE_UUID_SUFFIX = "6085-4726-be45-040c957391b5" +PLEJD_SERVICE = "31ba0001-" + BLE_UUID_SUFFIX + + +class PlejdCharacteristic(str, Enum): + """Plejd characteristic UIIDs.""" + + DATA_UUID = "31ba0004-" + BLE_UUID_SUFFIX + LAST_DATA_UUID = "31ba0005-" + BLE_UUID_SUFFIX + AUTH_UUID = "31ba0009-" + BLE_UUID_SUFFIX + PING_UUID = "31ba000a-" + BLE_UUID_SUFFIX + + +class PlejdCommand(int, Enum): + """Plejd BLE commands.""" + + BLE_CMD_DIM_CHANGE = 0x00C8 + BLE_CMD_DIM2_CHANGE = 0x0098 + BLE_CMD_STATE_CHANGE = 0x0097 + BLE_CMD_SCENE_TRIG = 0x0021 + BLE_CMD_TIME_UPDATE = 0x001B + BLE_CMD_REMOTE_CLICK = 0x0016 + + +class PlejdLightAction(str, Enum): + """BLE payload for possible actions on a light.""" + + BLE_DEVICE_ON = "01" + BLE_DEVICE_OFF = "00" + BLE_DEVICE_DIM = "01" + + +class PlejdResponse(int, Enum): + """Possible Plejd device responses.""" + + BLE_REQUEST_NO_RESPONSE = 0x0110 + BLE_REQUEST_RESPONSE = 0x0102 + + +#################################################################################################### +# Plejd MISC +#################################################################################################### + + +# Traits +class PlejdTraits(int, Enum): + """Plejd device traits.""" + + NO_LOAD = 0 + NON_DIMMABLE = 9 # TODO: Not used yet + DIMMABLE = 11 # TODO: Not used yet + + +# Device types +class PlejdType(str, Enum): + """Plejd device types.""" + + SWITCH = "switch" + LIGHT = "light" + SENSOR = "sensor" + DEVICE_TRIGGER = "device_automation" + UNKNOWN = "unknown" + + +#################################################################################################### +# Project related constants +#################################################################################################### + +settings_dir: str = os.path.expanduser("~/.plejd/") + +settings_file: str = os.path.join(settings_dir, "settings.yaml") diff --git a/plejd_mqtt_ha/mdl/__init__.py b/plejd_mqtt_ha/mdl/__init__.py new file mode 100644 index 0000000..e562863 --- /dev/null +++ b/plejd_mqtt_ha/mdl/__init__.py @@ -0,0 +1 @@ +"""The plejd_mqtt_ha model package.""" diff --git a/plejd_mqtt_ha/mdl/bt_data_type.py b/plejd_mqtt_ha/mdl/bt_data_type.py new file mode 100644 index 0000000..7434ce3 --- /dev/null +++ b/plejd_mqtt_ha/mdl/bt_data_type.py @@ -0,0 +1,44 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Type to hold parsed data coming from a plejd device. + +All new BT devices should add a new class here, inheriting from BTData. +""" + +from pydantic import BaseModel + + +class BTData(BaseModel): + """Type to hold parsed data coming from a plejd device.""" + + raw_data: bytearray + """Raw, decrypted data from the Plejd device + """ + + class Config: + """Pydantic BaseModel configuration.""" + + arbitrary_types_allowed = True # Allow for bytearray + + +class BTLightData(BTData): + """Parsed data type coming from a plejd light.""" + + state: bool + """State of the light, True = light is ON + """ + brightness: int + """Brightness of the Plejd light + """ diff --git a/plejd_mqtt_ha/mdl/bt_device.py b/plejd_mqtt_ha/mdl/bt_device.py new file mode 100644 index 0000000..a3bb6f5 --- /dev/null +++ b/plejd_mqtt_ha/mdl/bt_device.py @@ -0,0 +1,213 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Plejd bluetooth device. + +All new BT devices should add a new class here, inheriting from BTDevice. +""" + +import logging +from typing import Callable, Generic, TypeVar + +from plejd_mqtt_ha import constants +from plejd_mqtt_ha.bt_client import ( + BTClient, + PlejdBluetoothError, + PlejdNotConnectedError, + PlejdTimeoutError, +) +from plejd_mqtt_ha.mdl.bt_data_type import BTData, BTLightData +from plejd_mqtt_ha.mdl.bt_device_info import BTDeviceInfo, BTLightInfo + +PlejdDeviceTypeT = TypeVar("PlejdDeviceTypeT", bound=BTDeviceInfo) + + +class BTDevice(Generic[PlejdDeviceTypeT]): + """Plejd bluetooth device super class.""" + + def __init__(self, bt_client: BTClient, device_info: PlejdDeviceTypeT) -> None: + """Initialize the BTDevice instance. + + Parameters + ---------- + bt_client : BTClient + BTClient instance to use for communication + device_info : PlejdDeviceTypeT + Device info for the device + """ + self._device_info = device_info + self._plejd_bt_client = bt_client + + async def subscribe(self, callback: Callable[[BTData], None]) -> bool: + """Subscribe to changes for the device. + + Parameters + ---------- + callback : Callable[[PlejdResponseType], None] + Callback to be invoked when data is received + + Returns + ------- + bool + Boolean status of the operation + """ + + def _proxy_callback(decrypted_data: bytearray) -> None: + callback(self._decode_response(decrypted_data)) # return parsed data + + return await self._plejd_bt_client.subscribe_last_data( + self._device_info.ble_address, _proxy_callback + ) + + def _decode_response(self, decrypted_data: bytearray) -> BTLightData: + """Device specific decoding to be implemented by subclass, ie device class. + + Parameters + ---------- + decrypted_data : bytearray + Decrypted data coming from Plejd device + + Raises + ------ + NotImplementedError + In case it's not implemented in subclass but a callback is provided + """ + raise NotImplementedError + + +class BTLight(BTDevice[BTLightInfo]): + """Plejd bluetooth light device.""" + + async def on(self) -> bool: + """Turn on a physical Plejd Light. + + Returns + ------- + bool + True if successful, False otherwise + """ + logging.debug(f"Turning on device {self._device_info.name}") + try: + await self._plejd_bt_client.send_command( + self._device_info.ble_address, + constants.PlejdCommand.BLE_CMD_STATE_CHANGE, + constants.PlejdLightAction.BLE_DEVICE_ON, + constants.PlejdResponse.BLE_REQUEST_NO_RESPONSE, + ) + except PlejdNotConnectedError as err: + logging.warning( + f"Device {self._device_info.name} is not connected, cannot turn on." + f"Error: {str(err)}" + ) + return False + except (PlejdBluetoothError, PlejdTimeoutError) as err: + logging.warning( + f"Failed to turn on device {self._device_info.name}, due to bluetooth a error." + f"Error: {str(err)}" + ) + return False + + return True + + async def off(self) -> bool: + """Turn off a physical Plejd Light. + + Returns + ------- + bool + True if successful, False otherwise + """ + logging.debug(f"Turning off device {self._device_info.name}") + + try: + await self._plejd_bt_client.send_command( + self._device_info.ble_address, + constants.PlejdCommand.BLE_CMD_STATE_CHANGE, + constants.PlejdLightAction.BLE_DEVICE_OFF, + constants.PlejdResponse.BLE_REQUEST_NO_RESPONSE, + ) + except PlejdNotConnectedError as err: + logging.warning( + f"Device {self._device_info.name} is not connected, cannot turn on." + f"Error: {str(err)}" + ) + return False + except (PlejdBluetoothError, PlejdTimeoutError) as err: + logging.warning( + f"Failed to turn on device {self._device_info.name}, due to bluetooth a error." + f"Error: {str(err)}" + ) + return False + + return True + + async def brightness(self, brightness: int) -> bool: + """Set brightness of a Plejd Light. + + Parameters + ---------- + brightness : int + Brightness to set, 0-255 + + Returns + ------- + bool + True if successful, False otherwise + """ + logging.debug(f"Setting brightness of device {self._device_info.name}") + + if not self._device_info.brightness: + raise RuntimeError( + f"Device {self._device_info.name} does not support setting brightness" + ) + + pad = "0" if brightness <= 0xF else "" + s_brightness = brightness << 8 | brightness + data = constants.PlejdLightAction.BLE_DEVICE_DIM + pad + f"{s_brightness:X}" + try: + await self._plejd_bt_client.send_command( + self._device_info.ble_address, + constants.PlejdCommand.BLE_CMD_DIM2_CHANGE, + data, + constants.PlejdResponse.BLE_REQUEST_NO_RESPONSE, + ) + except PlejdNotConnectedError as err: + logging.warning( + f"Device {self._device_info.name} is not connected, cannot turn on." + f"Error: {str(err)}" + ) + return False + except (PlejdBluetoothError, PlejdTimeoutError) as err: + logging.warning( + f"Failed to turn on device {self._device_info.name}, due to bluetooth a error." + f"Error: {str(err)}" + ) + return False + + return True + + def _decode_response(self, decrypted_data: bytearray) -> BTLightData: + # Overriden + + state = decrypted_data[5] if len(decrypted_data) > 5 else 0 + brightness = decrypted_data[7] if len(decrypted_data) > 7 else 0 + + brightness = int(brightness) + state = bool(state) + response = BTLightData( + raw_data=decrypted_data, + state=state, + brightness=brightness, + ) + return response diff --git a/plejd_mqtt_ha/mdl/bt_device_info.py b/plejd_mqtt_ha/mdl/bt_device_info.py new file mode 100644 index 0000000..bf042be --- /dev/null +++ b/plejd_mqtt_ha/mdl/bt_device_info.py @@ -0,0 +1,63 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Type to hold information about a Plejd device. + +All new BT devices should add a new class here, inheriting from BTDeviceInfo. +""" + +from typing import Optional + +from plejd_mqtt_ha import constants +from pydantic import BaseModel + + +class BTDeviceInfo(BaseModel): + """Base class that defines device information common to all plejd devices.""" + + category: str + """Type of device category, ie light etc""" + supported_commands: Optional[list] = None + """List of Plejd BLE commands to listen to, device type specific""" + + model: str + """Model of the device""" + device_id: str + """Identity of the device, as stated in the Plejd API""" + unique_id: str + """Unique identity of the device, required by HA""" + name: str + """Name of the device""" + hardware_id: str # TODO rename to device address? + """Adress of the device within the Plejd mesh""" + index: int # TODO this is the index of the entity actually + """Index of the entity belonging to the device""" + ble_address: int + """BLE address of the device""" + firmware_version: Optional[str] = None + """Firmware version of the device""" + + +class BTLightInfo(BTDeviceInfo): + """Information specific to light devices.""" + + category = "light" + supported_commands = [ + constants.PlejdCommand.BLE_CMD_DIM2_CHANGE, + constants.PlejdCommand.BLE_CMD_DIM_CHANGE, + constants.PlejdCommand.BLE_CMD_STATE_CHANGE, + ] + + brightness: bool = False + """Whether or not the light supports setting brightness""" diff --git a/plejd_mqtt_ha/mdl/combined_device.py b/plejd_mqtt_ha/mdl/combined_device.py new file mode 100644 index 0000000..8bc66da --- /dev/null +++ b/plejd_mqtt_ha/mdl/combined_device.py @@ -0,0 +1,250 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Combined device module, combines a Plejd BT device with an MQTT device. + +All new devices should be added here, inheriting from CombinedDevice. +""" + +import asyncio +import json +import logging +from typing import Generic, Optional, TypeVar + +from ha_mqtt_discoverable import DeviceInfo, Discoverable, Settings +from ha_mqtt_discoverable.sensors import Light, LightInfo, Subscriber +from paho.mqtt.client import Client, MQTTMessage +from plejd_mqtt_ha.bt_client import BTClient, PlejdNotConnectedError +from plejd_mqtt_ha.mdl.bt_data_type import BTLightData +from plejd_mqtt_ha.mdl.bt_device import BTDevice, BTLight +from plejd_mqtt_ha.mdl.bt_device_info import BTDeviceInfo, BTLightInfo +from plejd_mqtt_ha.mdl.settings import PlejdSettings + +PlejdDeviceTypeT = TypeVar("PlejdDeviceTypeT", bound=BTDeviceInfo) +MQTTDeviceTypeT = TypeVar("MQTTDeviceTypeT", bound=Discoverable) + + +class CombinedDeviceError(Exception): + """Combined device error.""" + + def __init__(self, message: str) -> None: + """Initialize the CombinedDeviceError instance. + + Parameters + ---------- + message : str + Error message + """ + self.message = message + super().__init__(self.message) + + +class MQTTDeviceError(CombinedDeviceError): + """MQTT device error.""" + + pass + + +class BTDeviceError(CombinedDeviceError): + """MQTT device error.""" + + pass + + +class CombinedDevice(Generic[PlejdDeviceTypeT]): + """A combined device that connects a Plejd BT device to an MQTT devices.""" + + def __init__( + self, + bt_client: BTClient, + settings: PlejdSettings, + device_info: PlejdDeviceTypeT, + ) -> None: + """Initialize the CombinedDevice instance. + + Parameters + ---------- + bt_client : BTClient + BTClient instance to use for communication + settings : PlejdSettings + Settings for the device + device_info : PlejdDeviceTypeT + Device info for the device + """ + self._device_info = device_info + self._settings = settings + self._plejd_bt_client = bt_client + self._event_loop = asyncio.get_event_loop() + self._mqtt_device = None + self._bt_device: Optional[BTDevice] = None + + async def start(self) -> None: + """Start the combined device. + + This will register it with HA and connect it to the physical device. + """ + try: + self._mqtt_device = self._create_mqtt_device() + except ConnectionError as err: + error_msg = f"Failed to connect to MQTT broker for {self._device_info.name}" + logging.error(error_msg) + raise MQTTDeviceError(error_msg) from err + except RuntimeError as err: + error_msg = f"Failed to create MQTT device for {self._device_info.name}" + logging.error(error_msg) + raise MQTTDeviceError(error_msg) from err + + try: + self._bt_device = await self._create_bt_device() + except BTDeviceError as err: + error_msg = f"Failed to create BT device for {self._device_info.name}" + logging.error(error_msg) + raise BTDeviceError(error_msg) from err + + def _create_mqtt_device(self) -> Subscriber[PlejdDeviceTypeT]: + """Create an MQTT device, shall be overriden in all subclasses. + + If a callbac is needed, use the _mqtt_callback function. + + Returns + ------- + Any + The created MQTT device + + Raises + ------ + NotImplementedError + Must be implemented in subclass, otherwise we raise an exception + """ + raise NotImplementedError + + async def _create_bt_device(self) -> BTDevice: + """Create a Plejd BT mesh device, shall be overriden in all subclasses. + + Returns + ------- + Optional[BTDevice] + The created Plejd BT mesh device + + Raises + ------ + NotImplementedError + Must be implemented in subclass, otherwise we raise an exception + """ + raise NotImplementedError + + def _mqtt_callback(self, client: Client, user_data, message: MQTTMessage) -> None: + """MQTT device callback, shall be implemented by subclass if an MQTT callback is needed. + + Parameters + ---------- + client : Client + MQTT client + user_data : _type_ + Optional user data + message : MQTTMessage + Received MQTT message + + Raises + ------ + NotImplementedError + If used and not implemented, raise an exception + """ + raise NotImplementedError + + def _bt_callback(self, light_response: BTLightData) -> None: + """Plejd BT mesh device callback, shall be implemented if subclass needs a callback. + + Parameters + ---------- + light_response : PlejdLightResponse + Data coming from Plejd BT mesh device + + Raises + ------ + NotImplementedError + If used and not implemented, raise an exception + """ + raise NotImplementedError + + +# TODO create subclass from device category automatically??? Possible? +class CombinedLight(CombinedDevice[BTLightInfo]): + """A combined Plejd BT and MQTT light.""" + + def _create_mqtt_device(self) -> Subscriber[BTLightInfo]: + # Override + mqtt_device_info = DeviceInfo( + name=self._device_info.name, identifiers=self._device_info.unique_id + ) + supported_color_modes = None + if self._device_info.brightness: + supported_color_modes = ["brightness"] + + mqtt_light_info = LightInfo( + name=self._device_info.name, + brightness=self._device_info.brightness, + color_mode=self._device_info.brightness, + supported_color_modes=supported_color_modes, + unique_id=self._device_info.unique_id + "_1", + device=mqtt_device_info, + ) + settings = Settings(mqtt=self._settings.mqtt, entity=mqtt_light_info) + return Light(settings=settings, command_callback=self._mqtt_callback) + + async def _create_bt_device(self) -> BTDevice: + # Override + bt_light = BTLight(bt_client=self._plejd_bt_client, device_info=self._device_info) + logging.info(f"Subscribing to BT device {self._device_info.name}") + + try: + await bt_light.subscribe(self._bt_callback) + except PlejdNotConnectedError as err: + error_message = f"Failed to subscribe to BT data for device {self._device_info.name}" + logging.error(error_message) + raise BTDeviceError(error_message) from err + + return bt_light + + def _mqtt_callback(self, client: Client, user_data, message: MQTTMessage) -> None: + # Override + payload = json.loads(message.payload.decode()) + if not self._bt_device: + logging.info(f"BT device {self._device_info.name} not created yet") + return + if "brightness" in payload: + if asyncio.run_coroutine_threadsafe( + self._bt_device.brightness(payload["brightness"]), loop=self._event_loop + ): + self._mqtt_device.brightness(payload["brightness"]) # TODO generics? + elif "state" in payload: + if payload["state"] == "ON": + if asyncio.run_coroutine_threadsafe(self._bt_device.on(), loop=self._event_loop): + self._mqtt_device.on() + else: + if asyncio.run_coroutine_threadsafe(self._bt_device.off(), loop=self._event_loop): + self._mqtt_device.off() + else: + logging.warning(f"Unknown payload {payload}") + + def _bt_callback(self, light_response: BTLightData) -> None: + # Override + if not self._mqtt_device: + logging.info(f"MQTT device {self._device_info.name} not created yet") + return + + if light_response.state: + self._mqtt_device.brightness(light_response.brightness) + else: + self._mqtt_device.off() diff --git a/plejd_mqtt_ha/mdl/settings.py b/plejd_mqtt_ha/mdl/settings.py new file mode 100644 index 0000000..27e621c --- /dev/null +++ b/plejd_mqtt_ha/mdl/settings.py @@ -0,0 +1,110 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Settings module for Plejd MQTT HA. + +Settings are loaded from a YAML file, the default location is ~/.plejd/settings.yaml +""" + + +import os +from typing import Optional + +from pydantic import BaseModel, validator + + +class API(BaseModel): + """Settings related to Plejd API.""" + + user: str + """Plejd user name (email)""" + password: str + """Password of the plejd user""" + site: Optional[str] = None + """Name of the plejd site to use, default is the first in the accounts list""" + timeout: float = 10.0 + """Timeout to reach Plejd API""" + cache_policy: str = "FIRST_CACHE" + """ + Cache policy to use. Can be one of the following: + - "NO_CACHE": Does not use cache. + - "FIRST_CACHE": Caches the plejd site on first run, then uses cache. + - "NEEDED_CACHE": Uses cached site only when network is not available. + """ + cache_dir: str = os.path.expanduser("~/.plejd/") + """Directory to store cached site, not used if cache_policy is set to NO_CACHE""" + cache_file: str = "site.json" + """File name for cached site, not used if cache_policy is set to NO_CACHE""" + + @validator("cache_policy") + def cache_policy_is_valid(cls, v): + """Validate cache policy.""" + if v not in ["NO_CACHE", "FIRST_CACHE", "NEEDED_CACHE"]: + raise ValueError("Invalid cache policy") + return v + + +class MQTT(BaseModel): + """Settings related to MQTT broker.""" + + host: str = "localhost" + """Address of the host MQTT broker""" + port: int = 1883 + """Port of the MQTT host""" + user: Optional[str] = None + """MQTT user name""" + password: Optional[str] = None + """Password of the MQTT user""" + ha_discovery_prefix: str = "homeassistant" + """Home assistant discovery prefix""" + + +class BLE(BaseModel): + """Settings related to BLE.""" + + adapter: Optional[str] = None # TODO: implement + """If a specific bluetooth adapter is to be used""" + preferred_device: Optional[str] = None # TODO: implement + """If a specific Plejd device is to be used as mesh ingress point, if not set the device with + the strongest signal will be used. Not recommended to use this setting""" + scan_time: float = 10.0 + """Time to scan for plejd bluetooth devices""" + retries: int = 10 + """Number of times to try and reconnect to Plejd mesh""" + time_retries: float = 10.0 + """Time between retries""" + + +class PlejdSettings(BaseModel): + """Single class containing all settings.""" + + api: API + """Plejd API settings""" + mqtt: MQTT = MQTT() + """MQTT settings""" + ble: BLE = BLE() + """BLE settings""" + + health_check: bool = True + """Enable health check""" + health_check_interval: float = 60.0 + """Interval in seconds between writing health check files""" + health_check_dir: str = os.path.expanduser("~/.plejd/") + """Directory to store health check files""" + health_check_bt_file: str = "bluetooth" + """File name for bluetooth health check""" + health_check_mqtt_file: str = "mqtt" + """File name for MQTT health check""" + health_check_hearbeat_file: str = "heartbeat" + """File name for heartbeat file""" diff --git a/plejd_mqtt_ha/mdl/site.py b/plejd_mqtt_ha/mdl/site.py new file mode 100644 index 0000000..840a95f --- /dev/null +++ b/plejd_mqtt_ha/mdl/site.py @@ -0,0 +1,31 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Type to hold parsed data from plejd cloud, containing information about an entire site.""" + +from plejd_mqtt_ha.mdl.bt_device_info import BTDeviceInfo +from pydantic import BaseModel + + +class PlejdSite(BaseModel): + """Holds important data related to a Plejd site.""" + + name: str + """Name of the site""" + site_id: str + """Identity of the site""" + crypto_key: str + """Crypto key used to authenticate against the plejd mesh""" + devices: list[BTDeviceInfo] + """List of devices belonging to the site""" diff --git a/plejd_mqtt_ha/plejd.py b/plejd_mqtt_ha/plejd.py new file mode 100644 index 0000000..37cedc3 --- /dev/null +++ b/plejd_mqtt_ha/plejd.py @@ -0,0 +1,232 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Main plejd module. + +Responsible for starting loading settings, performing health checks and creating devices. +""" + +import asyncio +import logging +import os +import sys +import time + +import yaml +from plejd_mqtt_ha import constants +from plejd_mqtt_ha.bt_client import BTClient +from plejd_mqtt_ha.mdl.combined_device import ( + BTDeviceError, + CombinedDevice, + CombinedLight, + MQTTDeviceError, +) +from plejd_mqtt_ha.mdl.settings import PlejdSettings +from plejd_mqtt_ha.mdl.site import PlejdSite +from plejd_mqtt_ha.plejd_api import IncorrectCredentialsError, PlejdAPI, PlejdAPIError +from pydantic import ValidationError + + +def start() -> None: + """Start the Plejd service. + + This function will never return unless program exits, for whatever reason. + """ + logging.basicConfig(level=logging.INFO) + try: + asyncio.run(_run()) + except Exception as ex: + logging.critical("Unhandled exception occured, exiting") + logging.critical(ex) + sys.exit() + finally: + pass # TODO: Cleanup + + +async def _run() -> None: + """Entry point for starting and running the program.""" + try: + with open(constants.settings_file, "r") as file: + settings_yaml = yaml.safe_load(file) + + plejd_settings = PlejdSettings.parse_obj(settings_yaml) + except FileNotFoundError: + logging.critical("The settings.yaml file was not found.") + return + except yaml.YAMLError: + logging.critical("There was an error parsing the settings.yaml file.") + return + except ValidationError as e: + logging.critical( + f"There was an error parsing the settings into a PlejdSettings object: {e}" + ) + return + + logging.info("Loaded Plejd settings.. [OK]") + + try: + plejd_site = await _load_plejd_site(plejd_settings) + except IncorrectCredentialsError as err: + logging.critical(str(err)) + return # exit program if incorrect credentials + + logging.info("Plejd site loaded.. [OK]") + + plejd_bt_client = await _create_bt_client(plejd_site.crypto_key, plejd_settings) + + logging.info("Created Plejd BT client.. [OK]") + discovered_devices = await _create_devices(plejd_settings, plejd_bt_client, plejd_site) + if len(discovered_devices) <= 0: # No devices created + logging.critical("Failed to create Plejd devices, exiting") + return + logging.info(f"Created {len(discovered_devices)} Plejd devices.. [OK]") + + heartbeat_task = asyncio.create_task( + write_health_data(plejd_settings, discovered_devices) + ) # Create heartbeat task + + await heartbeat_task # Wait indefinitely for the heartbeat task + + +async def write_health_data( + plejd_settings: PlejdSettings, discovered_devices: list[CombinedDevice] +) -> None: + """Write health data to files. + + Will continously write health data to files in the health_check_dir. This is used by the docker + healthcheck script. + + Parameters + ---------- + plejd_settings : PlejdSettings + Settings + discovered_devices : list[CombinedDevice] + List of discovered devices + """ + first_device = discovered_devices[0] # Get first device + bt_client = first_device._plejd_bt_client # Get BT client + mqtt_client = first_device._mqtt_device.mqtt_client # Get MQTT client + + # Write bluetooth health check file + bt_health_check_file = plejd_settings.health_check_dir + plejd_settings.health_check_bt_file + mqtt_health_check_file = plejd_settings.health_check_dir + plejd_settings.health_check_mqtt_file + heartbeat_file = plejd_settings.health_check_dir + plejd_settings.health_check_hearbeat_file + + while True: # Run forever + # Retrieve status of BT and MQTT clients + if bt_client.is_connected() and await bt_client.ping(): + bt_status = "connected" + else: + bt_status = "disconnected" + if mqtt_client.is_connected(): + mqtt_status = "connected" + else: + mqtt_status = "disconnected" + + try: + if plejd_settings.health_check: + if not os.path.exists(plejd_settings.health_check_dir): + os.makedirs(plejd_settings.health_check_dir) + # Indicate that the program is running + with open(heartbeat_file, "w") as f: + f.write(str(time.time())) + # Indicate that we are connected to Plejd bluetooth mesh + with open(bt_health_check_file, "w") as f: + f.write(bt_status) + # Indicate that we are connected to broker + with open(mqtt_health_check_file, "w") as f: + f.write(mqtt_status) + else: + pass # Do nothing if health check is disabled + except (IOError, FileNotFoundError) as e: + logging.error(f"Error writing to healthcheck file: {e}") + finally: + await asyncio.sleep(plejd_settings.health_check_interval) + + +async def _load_plejd_site(settings: PlejdSettings) -> PlejdSite: + # Load Plejd site from API, retry until success, or exit if credentials are incorrect + api = PlejdAPI(settings) + + try: + plejd_site = api.get_site() + except IncorrectCredentialsError: + logging.critical("Failed to login to Plejd API, incorrect credentials in settings.json") + raise + except PlejdAPIError as err: # Any other error, retry forever + logging.error(f"Failed to retreive Plejd site: {str(err)})") + + async def _api_retry_loop() -> PlejdSite: + logging.info("Entering Plejd API retry loop") + while True: + try: + return api.get_site() + except PlejdAPIError as err: + logging.error(f"Failed to login to Plejd API: {str(err)})") + await asyncio.sleep(10) # TODO: HC value, add backoff? + + plejd_site = await _api_retry_loop() # retry until success + + logging.debug("Successfully logged in to Plejd API") + + return plejd_site + + +async def _create_bt_client(crypto_key: str, settings: PlejdSettings) -> BTClient: + # Create Plejd BT client, retry until success + + bt_client = BTClient(crypto_key, settings) + stay_connected = True + + if not await bt_client.connect(stay_connected): + + async def _bt_retry_loop() -> None: + logging.info("Entering Plejd BT retry loop") + while True: + if await bt_client.connect(stay_connected): + return + await asyncio.sleep(10) # TODO: HC value, add backoff? + + await _bt_retry_loop() + + return bt_client + + +async def _create_devices( + settings: PlejdSettings, bt_client: BTClient, plejd_site: PlejdSite +) -> list[CombinedDevice]: + # Create every device connected to the Plejd Account + + combined_devices = [] + for device in plejd_site.devices: + logging.info(f"Creating device {device.name}") + if device.category == "light": # TODO HC for now + combined_device = CombinedLight( + bt_client=bt_client, settings=settings, device_info=device + ) + try: + await combined_device.start() + combined_devices.append(combined_device) + except MQTTDeviceError as err: + logging.warning( + f"Skipping device {device.name}, cant create MQTT device: {str(err)}" + ) + continue + except BTDeviceError as err: + logging.warning(f"Skipping device {device.name}, cant create BT device: {str(err)}") + continue + else: + logging.warning(f"{device.category} not supported") + continue + return combined_devices diff --git a/plejd_mqtt_ha/plejd_api.py b/plejd_mqtt_ha/plejd_api.py new file mode 100644 index 0000000..a25d865 --- /dev/null +++ b/plejd_mqtt_ha/plejd_api.py @@ -0,0 +1,659 @@ +# Copyright 2023 Viktor Karlquist +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Plejd API module. + +Handles all communication with the Plejd cloud platform. +""" + +import json +import logging +from typing import Optional + +import plejd_mqtt_ha.constants +import requests +from plejd_mqtt_ha.mdl.bt_device_info import BTDeviceInfo, BTLightInfo +from plejd_mqtt_ha.mdl.settings import PlejdSettings +from plejd_mqtt_ha.mdl.site import PlejdSite +from pydantic import BaseModel + + +class PlejdAPIError(Exception): + """Exception raised for errors in the Plejd Bluetooth connection.""" + + def __init__(self, message: str): + """Initialize PlejdAPIError class. + + Parameters + ---------- + message : str + Error message + """ + self.message = message + super().__init__(self.message) + + +class UnknownResponseError(PlejdAPIError): + """Exception raised when the Plejd API returns an unknown response.""" + + pass + + +class UnsupportedDeviceTypeError(PlejdAPIError): + """Exception raised when the Plejd API returns an unsupported device type.""" + + pass + + +class IncorrectCredentialsError(PlejdAPIError): + """Exception raised when the supplied credentials are incorrect.""" + + pass + + +class CacheFileError(PlejdAPIError): + """Exception raised when cache file cannot be loaded or stored.""" + + pass + + +class PlejdAPI: + """PlejdAPI class that handles all communication with the Plejd cloud platform. + + Data does not need to be fetched unless devices has been added and/or modified, ie data can be + cached locally. + """ + + class PlejdDeviceType(BaseModel): + """Types of devices from Plejd API.""" + + name: str + """Name of the specific type + """ + device_category: str + """Which type of device category does it belong to + """ + dimmable: bool = False + """Whether or not the device can be dimmed + """ + broadcast_clicks: bool = False + """Whether or not it can broadcast clicks, ie a button etc + """ + + def __init__(self, settings: PlejdSettings) -> None: + """Initialize PlejdAPI class. + + Parameters + ---------- + settings : PlejdSettings + Settings for the Plejd API + """ + self._settings = settings.api + self._session_token = None + + def login(self) -> None: + """Login to plejd API. This is required to get the site information.""" + headers = { + "X-Parse-Application-Id": plejd_mqtt_ha.constants.API_APP_ID, + "Content-Type": "application/json", + } + + data = {"username": self._settings.user, "password": self._settings.password} + + try: + response = requests.post( + plejd_mqtt_ha.constants.API_BASE_URL + plejd_mqtt_ha.constants.API_LOGIN_URL, + json=data, + headers=headers, + timeout=self._settings.timeout, + ) + response.raise_for_status() + response_json = response.json() + if "sessionToken" not in response_json: + raise ValueError("Failed to parse response as JSON") + except requests.HTTPError as err: + if response.status_code == 401: + logging.error("Login failed due to incorrect credentials") + raise IncorrectCredentialsError( + "Login failed due to incorrect credentials" + ) from err + else: + logging.error(f"HTTP error occurred: {str(err)}") + raise PlejdAPIError(f"HTTP error occurred: {str(err)}") from err + except requests.RequestException as err: + logging.error(f"Login request failed: {str(err)}") + raise PlejdAPIError(f"Login request failed: {str(err)}") from err + except ValueError as err: + logging.error(f"Login parse failed: {str(err)}") + raise UnknownResponseError(f"Login parse failed: {str(err)}") from err + + self._session_token = response_json["sessionToken"] + + def get_site(self) -> PlejdSite: + """Get plejd site. + + If no site name in settings, it will take the first site in the list. Also responsible for + logging in to the API if not already logged in. + + Returns + ------- + PlejdSite + Plejd site with site_name or first plejd site in list + + Raises + ------ + PlejdAPIError + If the request fails + UnknownResponseError + If the response is not as expected + CacheFileError + If the cache file is not found + IncorrectCredentialsError + If the credentials are incorrect to login to the Plejd API + """ + # Check cache policy + if self._settings.cache_policy == "FIRST_CACHE": + # Fetch from cache if exists, otherwise use cache + logging.info("Fetching site from cache if possible, otherwise using API") + try: + plejd_site = self._get_site_from_cache() + except CacheFileError as err: + logging.warning(f"Failed to get site from cache: {str(err)}") + logging.info("Fetching site from Plejd API") + try: + plejd_site = self._get_site_from_api() + except (IncorrectCredentialsError, PlejdAPIError, UnknownResponseError) as err: + logging.error(f"Failed to get site from Plejd API: {str(err)}") + raise + elif self._settings.cache_policy == "NO_CACHE": + # Fetch from API, otherwise raise error + logging.info("Fetching site from Plejd API") + try: + # Attempt to fetch from API, do not store site afterwards + plejd_site = self._get_site_from_api() + except (IncorrectCredentialsError, PlejdAPIError, UnknownResponseError) as err: + logging.warning(f"Failed to get site from API: {str(err)}") + raise + elif self._settings.cache_policy == "NEEDED_CACHE": + # Fetch from API, otherwise use cache + logging.info("Fetching site from API if possible, otherwise using cache") + try: + plejd_site = self._get_site_from_api() + except (IncorrectCredentialsError, PlejdAPIError, UnknownResponseError) as err: + logging.warning(f"Failed to get site from API: {str(err)}") + logging.info("Fetching site from cache") + try: + plejd_site = self._get_site_from_cache() + except CacheFileError as err: + logging.error(f"Failed to get site from cache: {str(err)}") + raise + + return plejd_site + + def _get_site_from_cache(self) -> PlejdSite: + """Get site from cache. + + Returns + ------- + PlejdSite + Plejd site + """ + json_site = self._get_json_site_from_cache() + + plejd_site = PlejdSite( + name=json_site["site"]["title"], + site_id=json_site["site"]["siteId"], + crypto_key=json_site["plejdMesh"]["cryptoKey"], + devices=self._get_devices(json_site), + ) + + return plejd_site + + def _get_site_from_api(self) -> PlejdSite: + """Get site from Plejd API. + + Returns + ------- + PlejdSite + Plejd site + """ + self.login() + site_data = self._get_site_data() + site_id = self._get_site_id(site_data, self._settings.site) + json_site = self._get_json_site_from_api(site_id) + + try: + self.store_json_site_to_cache(json_site) + except CacheFileError as err: + # Only log error if not able to store site to cache, ok to continue + logging.warning(f"Failed to store site to cache: {str(err)}") + + plejd_site = PlejdSite( + name=json_site["site"]["title"], + site_id=site_id, + crypto_key=json_site["plejdMesh"]["cryptoKey"], + devices=self._get_devices(json_site), + ) + + return plejd_site + + def _get_json_site_from_api(self, site_id: str) -> dict: + """Get json site data from Plejd API. + + Parameters + ---------- + site_id : str + Id of the site to fetch JSON content from + + Returns + ------- + str: + JSON content of the site + """ + headers = { + "X-Parse-Application-Id": plejd_mqtt_ha.constants.API_APP_ID, + "X-Parse-Session-Token": self._session_token, + "Content-Type": "application/json", + } + + data = { + "siteId": site_id, + } + + response = requests.post( + plejd_mqtt_ha.constants.API_BASE_URL + plejd_mqtt_ha.constants.API_SITE_DETAILS_URL, + json=data, + headers=headers, + timeout=10, + ) + try: + response_json = response.json() + if "result" not in response_json: + raise UnknownResponseError(f"Failed to get site: {response_json}") + logging.info("Got site from Plejd API") + except (ValueError, UnknownResponseError) as err: + logging.error(f"Failed to parse response as JSON: {str(err)}") + raise UnknownResponseError(f"Failed to parse response as JSON: {str(err)}") from err + + return response_json["result"][0] + + def _get_site_id(self, site_data: dict, site_name: Optional[str]) -> str: + """Get site id from site data. + + Will return the first site if no site name is specified in settings. + + Parameters + ---------- + site_data : dict + JSON content of the site + site_name : Optional[str] + Name of the site to fetch, by default None + + Returns + ------- + str + site id + """ + # Fetch site ID and name + site_id = "" + if site_name is not None: + for site in site_data: + if site["site"]["title"] == site_name: + site_id = site["site"]["siteId"] + break + if not site_id: + old_site_name = site_name # Store for warning message + site_name = site_data["result"][0]["site"]["title"] + site_id = site_data["result"][0]["site"]["site"]["siteId"] + logging.warning( + f"Site {old_site_name} not found in available sites, using {site_name}" + ) + else: + site_id = site_data[0]["site"]["siteId"] + return site_id + + def _get_site_data(self) -> dict: + """Get site data from Plejd API. + + Site data contains information about all available sites. + + Returns + ------- + dict + JSON content of the site + + Raises + ------ + PlejdAPIError + If the request fails + UnknownResponseError + If the response is not as expected + """ + # Fetch site information from Plejd API + headers = { + "X-Parse-Application-Id": plejd_mqtt_ha.constants.API_APP_ID, + "X-Parse-Session-Token": self._session_token, + "Content-Type": "application/json", + } + try: + response = requests.post( + plejd_mqtt_ha.constants.API_BASE_URL + plejd_mqtt_ha.constants.API_SITE_LIST_URL, + headers=headers, + timeout=10, + ) + response.raise_for_status() + response_json = response.json() + if "result" not in response_json: + raise UnknownResponseError("") + except requests.RequestException as err: + logging.error(f"Failed to get site: {str(err)}") + raise PlejdAPIError(f"Failed to get site: {str(err)}") from err + except (ValueError, UnknownResponseError) as err: + logging.error(f"Failed to parse site response: {str(err)}") + raise UnknownResponseError(f"Failed to parse site response: {str(err)}") from err + + return response_json["result"] + + def _get_json_site_from_cache(self) -> dict: + """Get site from cache. + + Returns + ------- + dict + JSON content of the site + """ + try: + with open(self._settings.cache_dir + self._settings.cache_file, "r") as file: + json_site = json.load(file) + logging.info("Loaded site from cache") + except (FileNotFoundError, IsADirectoryError, PermissionError, IOError) as err: + error_message = f"Failed to read cache file: {str(err)}" + logging.error(error_message) + raise CacheFileError(error_message) from err + return json_site + + def store_json_site_to_cache(self, json_site: dict) -> None: + """Store site to cache. + + Parameters + ---------- + json_site : dict + JSON content of the site + """ + try: + with open(self._settings.cache_dir + self._settings.cache_file, "w") as file: + json.dump(json_site, file) + except (FileNotFoundError, IsADirectoryError, PermissionError, IOError) as err: + error_message = f"Failed to write cache file: {str(err)}" + logging.error(error_message) + raise CacheFileError(error_message) from err + + def _get_devices(self, json_res: dict) -> list[BTDeviceInfo]: + """Get all devices from Plejd API. + + Parameters + ---------- + json_res : dict + JSON content of the site + + Returns + ------- + list[BTDeviceInfo] + List of all devices + """ + device_list = [] + plejd_devices = json_res["plejdDevices"] + output_address = json_res["outputAddress"] + device_address = json_res["deviceAddress"] + output_settings_list = json_res["outputSettings"] + input_settings_list = json_res["inputSettings"] + for device in json_res["devices"]: # Get all necessary plejd device details + device_id = device["deviceId"] + device_name = device["title"] + device_hardware_id = device_address[device_id] + + try: + device_type = self._get_device_type(device_hardware_id) + except UnsupportedDeviceTypeError as err: + logging.warning(f"Failed to get device type for {device_name}: {str(err)}") + continue + + devices = list(filter(lambda x: x["deviceId"] == device["deviceId"], plejd_devices))[:1] + if len(devices) > 0: + device_firmware_version = devices[0]["firmware"]["version"] + else: + logging.warning(f"Could not determine FW version for device: {device_name}") + device_firmware_version = "xxx" + + output_settings = list( + filter( + lambda x: x["deviceParseId"] == device["objectId"], + output_settings_list, + ) + )[:1] + input_settings = list( + filter(lambda x: x["deviceId"] == device["deviceId"], input_settings_list) + ) + + if len(output_settings) > 0: # It's an output device + if device_id not in output_address: + logging.warning( + f"Device: {device_name} does not exist as output address, skipping device" + ) + continue + + if device["traits"] == plejd_mqtt_ha.constants.PlejdTraits.NO_LOAD.value: + logging.warning( + f"No load settings found for output device: {device_name}, skipping device" + ) + continue + + device_index = output_settings[0][ + "output" + ] # TODO is this really the correct index?? + device_ble_address = output_address[device_id][str(device_index)] + # Append output index to device id for uniqueness + device_unique_id = device_id + f"_{device_index}" + elif len(input_settings) > 0: # It's an input device + if device_id not in device_address: + logging.warning( + f"Device: {device_name} does not exist as device address, skipping" + ) + continue + + device_ble_address = device_address[device_id] + for input_setting in input_settings: + if not device_type.broadcast_clicks: + logging.info( + f"Input device: {device_name} does not broadcast clicks, skipping" + ) + continue + + device_index = input_setting["input"] # TODO is this really the correct index?? + # Append input index to device id for uniqueness + device_unique_id = device_id + f"_{device_index}" + else: + logging.warning( + f"Device: {device_name} is neither output nor input device, skipping" + ) + continue + + # Parse type and create device info + if device_type.device_category == plejd_mqtt_ha.constants.PlejdType.LIGHT.value: + plejd_device = BTLightInfo( + model=device_type.name, + type=plejd_mqtt_ha.constants.PlejdType.LIGHT.value, + device_id=device_id, + unique_id=device_unique_id, + name=device_name, + hardware_id=device_hardware_id, + index=device_index, + ble_address=device_ble_address, + category=device_type.device_category, + firmware_version=device_firmware_version, + brightness=device_type.dimmable, + ) + elif device_type.device_category == plejd_mqtt_ha.constants.PlejdType.SWITCH.value: + # TODO create + logging.warning( + f"Usnupported device category {plejd_mqtt_ha.constants.PlejdType.SWITCH.value}" + ) + continue + elif device_type.device_category == plejd_mqtt_ha.constants.PlejdType.SENSOR.value: + # TODO + logging.warning( + f"Usnupported device category {plejd_mqtt_ha.constants.PlejdType.SENSOR.value}" + ) + continue + elif ( + device_type.device_category + == plejd_mqtt_ha.constants.PlejdType.DEVICE_TRIGGER.value + ): + # TODO + logging.warning( + "Usnupported device category " + f"{plejd_mqtt_ha.constants.PlejdType.DEVICE_TRIGGER.value}" + ) + continue + else: + continue + + device_list.append(plejd_device) + + return device_list + + def _get_device_type(self, hardware_id: int) -> PlejdDeviceType: + # Get type of device by hardware id + if hardware_id in {1, 11, 14}: + return self.PlejdDeviceType( + name="DIM-01", + device_category=plejd_mqtt_ha.constants.PlejdType.LIGHT.value, + dimmable=True, + broadcast_clicks=False, + ) + if hardware_id in {2, 15}: + return self.PlejdDeviceType( + name="DIM-02", + device_category=plejd_mqtt_ha.constants.PlejdType.LIGHT.value, + dimmable=True, + broadcast_clicks=False, + ) + if hardware_id == 3: + return self.PlejdDeviceType( + name="CTR-01", + device_category=plejd_mqtt_ha.constants.PlejdType.LIGHT.value, + dimmable=False, + broadcast_clicks=False, + ) + if hardware_id == 4: + return self.PlejdDeviceType( + name="GWY-01", + device_category=plejd_mqtt_ha.constants.PlejdType.SENSOR.value, + dimmable=False, + broadcast_clicks=False, + ) + if hardware_id == 5: + return self.PlejdDeviceType( + name="LED-10", + device_category=plejd_mqtt_ha.constants.PlejdType.LIGHT.value, + dimmable=True, + broadcast_clicks=False, + ) + if hardware_id == 6: + return self.PlejdDeviceType( + name="WPH-01", + device_category=plejd_mqtt_ha.constants.PlejdType.DEVICE_TRIGGER.value, + dimmable=False, + broadcast_clicks=True, + ) + if hardware_id == 7: + return self.PlejdDeviceType( + name="REL-01", + device_category=plejd_mqtt_ha.constants.PlejdType.SWITCH.value, + dimmable=False, + broadcast_clicks=False, + ) + if hardware_id == 8: + return self.PlejdDeviceType( + name="SPR-01", + device_category=plejd_mqtt_ha.constants.PlejdType.SWITCH.value, + dimmable=False, + broadcast_clicks=False, + ) + if hardware_id == 9: + return self.PlejdDeviceType( + name="WRT-01", + device_category=plejd_mqtt_ha.constants.PlejdType.DEVICE_TRIGGER.value, + dimmable=False, + broadcast_clicks=False, + ) + if hardware_id == 10: + return self.PlejdDeviceType( + name="WRT-01", + device_category=plejd_mqtt_ha.constants.PlejdType.DEVICE_TRIGGER.value, + dimmable=False, + broadcast_clicks=True, + ) + if hardware_id == 12: + return self.PlejdDeviceType( + name="DAL-01", + device_category=plejd_mqtt_ha.constants.PlejdType.LIGHT.value, + dimmable=False, + broadcast_clicks=False, + ) + if hardware_id == 13: + return self.PlejdDeviceType( + name="Generic", + device_category=plejd_mqtt_ha.constants.PlejdType.DEVICE_TRIGGER.value, + dimmable=False, + broadcast_clicks=True, + ) + if hardware_id == 16: + return self.PlejdDeviceType( + name="-unknown-", + device_category=plejd_mqtt_ha.constants.PlejdType.DEVICE_TRIGGER.value, + dimmable=False, + broadcast_clicks=True, + ) + if hardware_id == 17: + return self.PlejdDeviceType( + name="REL-01", + device_category=plejd_mqtt_ha.constants.PlejdType.SWITCH.value, + dimmable=False, + broadcast_clicks=False, + ) + if hardware_id == 18: + return self.PlejdDeviceType( + name="REL-02", + device_category=plejd_mqtt_ha.constants.PlejdType.SWITCH.value, + dimmable=False, + broadcast_clicks=False, + ) + if hardware_id == 19: + return self.PlejdDeviceType( + name="-unknown-", + device_category=plejd_mqtt_ha.constants.PlejdType.LIGHT.value, + dimmable=False, + broadcast_clicks=False, + ) + if hardware_id == 20: + return self.PlejdDeviceType( + name="SPR-01", + device_category=plejd_mqtt_ha.constants.PlejdType.SWITCH.value, + dimmable=False, + broadcast_clicks=False, + ) + error_message = f"Failed to get device type for device id {hardware_id}" + logging.error(error_message) + raise UnsupportedDeviceTypeError(error_message) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..963ee48 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1596 @@ +# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. + +[[package]] +name = "astroid" +version = "2.15.8" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, + {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, +] + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +wrapt = {version = ">=1.14,<2", markers = "python_version >= \"3.11\""} + +[[package]] +name = "bleak" +version = "0.21.1" +description = "Bluetooth Low Energy platform Agnostic Klient" +category = "main" +optional = false +python-versions = ">=3.8,<3.13" +files = [ + {file = "bleak-0.21.1-py3-none-any.whl", hash = "sha256:ccec260a0f5ec02dd133d68b0351c0151b2ecf3ddd0bcabc4c04a1cdd7f33256"}, + {file = "bleak-0.21.1.tar.gz", hash = "sha256:ec4a1a2772fb315b992cbaa1153070c7e26968a52b0e2727035f443a1af5c18f"}, +] + +[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 = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-CoreBluetooth = {version = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-libdispatch = {version = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} +typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.12\""} +"winrt-Windows.Devices.Bluetooth" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Bluetooth.Advertisement" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Enumeration" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Foundation" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Foundation.Collections" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Storage.Streams" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} + +[[package]] +name = "bleak-winrt" +version = "1.2.0" +description = "Python WinRT bindings for Bleak" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"}, + {file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"}, + {file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"}, + {file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"}, + {file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"}, + {file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"}, + {file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"}, + {file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"}, + {file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"}, + {file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"}, + {file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"}, +] + +[[package]] +name = "cerberus" +version = "1.3.5" +description = "Lightweight, extensible schema and data validation tool for Pythondictionaries." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "Cerberus-1.3.5-py3-none-any.whl", hash = "sha256:7649a5815024d18eb7c6aa5e7a95355c649a53aacfc9b050e9d0bf6bfa2af372"}, + {file = "Cerberus-1.3.5.tar.gz", hash = "sha256:81011e10266ef71b6ec6d50e60171258a5b134d69f8fb387d16e4936d0d47642"}, +] + +[[package]] +name = "certifi" +version = "2023.11.17" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "41.0.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "dateutils" +version = "0.6.12" +description = "Various utilities for working with date and datetime objects" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "dateutils-0.6.12-py2.py3-none-any.whl", hash = "sha256:f33b6ab430fa4166e7e9cb8b21ee9f6c9843c48df1a964466f52c79b2a8d53b3"}, + {file = "dateutils-0.6.12.tar.gz", hash = "sha256:03dd90bcb21541bd4eb4b013637e4f1b5f944881c46cc6e4b67a6059e370e3f1"}, +] + +[package.dependencies] +python-dateutil = "*" +pytz = "*" + +[[package]] +name = "dbus-fast" +version = "2.20.0" +description = "A faster version of dbus-next" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "dbus_fast-2.20.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:ecf22e22434bdd61bfb8b544eb58f5032b23dda5a7fc233afa1d3c9c3241f0a8"}, + {file = "dbus_fast-2.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70f4c1fe23c47a59d81c8fd8830c65307a1f089cc92949004df4c65c69f155"}, + {file = "dbus_fast-2.20.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:9963180456586d0e1b58075e0439a34ed8e9ee4266b35f76f3db6ffc1af17e27"}, + {file = "dbus_fast-2.20.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:eafbf4f0ac86fd959f86bbdf910bf64406b35315781014ef4a1cd2bb43985346"}, + {file = "dbus_fast-2.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bb668e2039e15f0e5af14bee7de8c8c082e3b292ed2ce2ceb3168c7068ff2856"}, + {file = "dbus_fast-2.20.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:3f7966f835da1d8a77c55a7336313bd97e7f722b316f760077c55c1e9533b0cd"}, + {file = "dbus_fast-2.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff856cbb1508bcf6735ed1e3c04de1def6c400720765141d2470e39c8fd6f13"}, + {file = "dbus_fast-2.20.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7a1da4ed9880046403ddedb7b941fd981872fc883dc9925bbf269b551f12120d"}, + {file = "dbus_fast-2.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9084ded47761a43b2252879c6ebaddb7e3cf89377cbdc981de7e8ba87c845239"}, + {file = "dbus_fast-2.20.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:d4b91b98cc1849f7d062d563d320594377b099ea9de53ebb789bf9fd6a0eeab4"}, + {file = "dbus_fast-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8ab58ef76575e6e00cf1c1f5747b24ce19e35d4966f1c5c3732cea2c3ed5e9"}, + {file = "dbus_fast-2.20.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1909addfad23d400d6f77c3665778a96003e32a1cddd1964de605d0ca400d829"}, + {file = "dbus_fast-2.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e591b218d4f327df29a89a922f199bbefb6f892ddc9b96aff21c05c15c0e5dc8"}, + {file = "dbus_fast-2.20.0-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:f55f75ac3891c161daeabdb37d8a3407098482fe54013342a340cdd58f2be091"}, + {file = "dbus_fast-2.20.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d317dba76e904f75146ce0c5f219dae44e8060767b3adf78c94557bbcbea2cbe"}, + {file = "dbus_fast-2.20.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:88126343024f280c1fadd6599ac4cd7046ed550ddc942811dc3d290830cffd51"}, + {file = "dbus_fast-2.20.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecc07860e3014607a5293e1b87294148f96b1cc508f6496b27e40f64079ebb7a"}, + {file = "dbus_fast-2.20.0-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:e9cdf34f81320b36ce7f2b8c46169632730d9cdcafc52b55cada95096fce3457"}, + {file = "dbus_fast-2.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40ad43412f92373e4c74bb76d2129a7f0c38a1d883adcfc08f168535f7e7846"}, + {file = "dbus_fast-2.20.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4a5fdebcd8f79d417693536d3ed08bb5842917d373fbc3e9685feecd001accd7"}, + {file = "dbus_fast-2.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b134d40688ca7f27ab38bec99194e2551c82fc01f583f44ae66129c3d15db8a7"}, + {file = "dbus_fast-2.20.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:fdd4ece2c856e44b5fe9dec354ce5d8930f7ae9bb4b96b3a195157621fea6322"}, + {file = "dbus_fast-2.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e609309d5503a5eab91a7b0cef9dd158c3d8786ac38643a962e99a69d5eb7a66"}, + {file = "dbus_fast-2.20.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8fd806bf4676a28b2323d8529d51f86fec5a9d32923d53ba522a4c2bc3d55857"}, + {file = "dbus_fast-2.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8526ff5b27b7c689d97fe8a29e97d3cb7298419b4cb63ed9029331d08d423c55"}, + {file = "dbus_fast-2.20.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:562868206d774080c4131b124a407350ffb5d2b89442048350b83b5084f4e0e1"}, + {file = "dbus_fast-2.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707fc61b4f2de83c8f574061fdaf0ac6fc28b402f451951cf0a1ead11bfcac71"}, + {file = "dbus_fast-2.20.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4a13c7856459e849202165fd9e1adda8169107a591b083b95842c15b9e772be4"}, + {file = "dbus_fast-2.20.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1aba69c1dd694399124efbc6ce15930e4697a95d527f16b614100f1f1055a2"}, + {file = "dbus_fast-2.20.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9817bd32d7734766b073bb08525b9560b0b9501c68c43cc91d43684a2829ad86"}, + {file = "dbus_fast-2.20.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bed226cccedee0c94b292e27fd1c7d24987d36b5ac1cde021031f9c77a76a423"}, + {file = "dbus_fast-2.20.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c11a2b4addb965e09a2d8d666265455f4a7e48916b7c6f43629b828de6682425"}, + {file = "dbus_fast-2.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88367c2a849234f134b9c98fdb16dc84d5ba9703fe995c67f7306900bfa13896"}, + {file = "dbus_fast-2.20.0.tar.gz", hash = "sha256:a38e837c5a8d0a1745ec8390f68ff57986ed2167b0aa2e4a79738a51dd6dfcc3"}, +] + +[[package]] +name = "dill" +version = "0.3.7" +description = "serialize all of Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, + {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[package]] +name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] + +[[package]] +name = "gitlike-commands" +version = "0.2.1" +description = "" +category = "main" +optional = false +python-versions = ">=3.9,<4.0" +files = [ + {file = "gitlike-commands-0.2.1.tar.gz", hash = "sha256:0e930493b36f970d41b10a692d94f0024fdd73ff35a264c0366b4363cccf5907"}, + {file = "gitlike_commands-0.2.1-py3-none-any.whl", hash = "sha256:f0a7da49eb9ee5da1171f4a19ca5abecfaeebfd1fb87b0a16a552ca825c44eb7"}, +] + +[[package]] +name = "ha-mqtt-discoverable" +version = "0.12.0" +description = "" +category = "main" +optional = false +python-versions = ">=3.10.0,<4.0" +files = [ + {file = "ha_mqtt_discoverable-0.12.0-py3-none-any.whl", hash = "sha256:2b8d322fa5dd2f87d9aec969815b7bf60ef0db6ac12b6ca2aed1cf3f1c5be7f7"}, + {file = "ha_mqtt_discoverable-0.12.0.tar.gz", hash = "sha256:331684180e0d39d87b9b4a52c7aeb594fffff737f5f880fd20a74001eec90856"}, +] + +[package.dependencies] +gitlike-commands = ">=0.2.1,<0.3.0" +paho-mqtt = ">=1.6.1,<2.0.0" +pyaml = ">=21.10.1,<22.0.0" +pydantic = ">=1.10.5,<2.0.0" +thelogrus = ">=0.7.0,<0.8.0" + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.13.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.0-py3-none-any.whl", hash = "sha256:15e0e937819b350bc256a7ae13bb25f4fe4f8871a0bc335b20c3627dba33f458"}, + {file = "isort-5.13.0.tar.gz", hash = "sha256:d67f78c6a1715f224cca46b29d740037bdb6eea15323a133e897cda15876147b"}, +] + +[package.dependencies] +pip-api = "*" +pipreqs = "*" +requirementslib = "*" + +[package.extras] +colors = ["colorama (>=0.4.6)"] +plugins = ["setuptools"] + +[[package]] +name = "lazy-object-proxy" +version = "1.9.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.7.1" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "numpy" +version = "1.26.2" +description = "Fundamental package for array computing in Python" +category = "main" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f"}, + {file = "numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523"}, + {file = "numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9"}, + {file = "numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8"}, + {file = "numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186"}, + {file = "numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec"}, + {file = "numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167"}, + {file = "numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36"}, + {file = "numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80"}, + {file = "numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841"}, + {file = "numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "paho-mqtt" +version = "1.6.1" +description = "MQTT version 5.0/3.1.1 client class" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "paho-mqtt-1.6.1.tar.gz", hash = "sha256:2a8291c81623aec00372b5a85558a372c747cbca8e9934dfe218638b8eefc26f"}, +] + +[package.extras] +proxy = ["PySocks"] + +[[package]] +name = "pep517" +version = "0.13.1" +description = "Wrappers to build Python packages using PEP 517 hooks" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pep517-0.13.1-py3-none-any.whl", hash = "sha256:31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721"}, + {file = "pep517-0.13.1.tar.gz", hash = "sha256:1b2fa2ffd3938bb4beffe5d6146cbcb2bda996a5a4da9f31abffd8b24e07b317"}, +] + +[[package]] +name = "pip" +version = "23.3.1" +description = "The PyPA recommended tool for installing Python packages." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pip-23.3.1-py3-none-any.whl", hash = "sha256:55eb67bb6171d37447e82213be585b75fe2b12b359e993773aca4de9247a052b"}, + {file = "pip-23.3.1.tar.gz", hash = "sha256:1fcaa041308d01f14575f6d0d2ea4b75a3e2871fe4f9c694976f908768e14174"}, +] + +[[package]] +name = "pip-api" +version = "0.0.30" +description = "An unofficial, importable pip API" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pip-api-0.0.30.tar.gz", hash = "sha256:a05df2c7aa9b7157374bcf4273544201a0c7bae60a9c65bcf84f3959ef3896f3"}, + {file = "pip_api-0.0.30-py3-none-any.whl", hash = "sha256:2a0314bd31522eb9ffe8a99668b0d07fee34ebc537931e7b6483001dbedcbdc9"}, +] + +[package.dependencies] +pip = "*" + +[[package]] +name = "pipreqs" +version = "0.4.13" +description = "Pip requirements.txt generator based on imports in project" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pipreqs-0.4.13-py2.py3-none-any.whl", hash = "sha256:e522b9ed54aa3e8b7978ff251ab7a9af2f75d2cd8de4c102e881b666a79a308e"}, + {file = "pipreqs-0.4.13.tar.gz", hash = "sha256:a17f167880b6921be37533ce4c81ddc6e22b465c107aad557db43b1add56a99b"}, +] + +[package.dependencies] +docopt = "*" +yarg = "*" + +[[package]] +name = "platformdirs" +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "plette" +version = "0.4.4" +description = "Structured Pipfile and Pipfile.lock models." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "plette-0.4.4-py2.py3-none-any.whl", hash = "sha256:42d68ce8c6b966874b68758d87d7f20fcff2eff0d861903eea1062126be4d98f"}, + {file = "plette-0.4.4.tar.gz", hash = "sha256:06b8c09eb90293ad0b8101cb5c95c4ea53e9b2b582901845d0904ff02d237454"}, +] + +[package.dependencies] +cerberus = {version = "*", optional = true, markers = "extra == \"validation\""} +tomlkit = "*" + +[package.extras] +tests = ["pytest", "pytest-cov", "pytest-xdist"] +validation = ["cerberus"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pyaml" +version = "21.10.1" +description = "PyYAML-based module to produce pretty and readable YAML-serialized data" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"}, + {file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"}, +] + +[package.dependencies] +PyYAML = "*" + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pydantic" +version = "1.10.13" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, + {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, + {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, + {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, + {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, + {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, + {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, + {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, + {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pylint" +version = "2.17.7" +description = "python code static checker" +category = "dev" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "pylint-2.17.7-py3-none-any.whl", hash = "sha256:27a8d4c7ddc8c2f8c18aa0050148f89ffc09838142193fdbe98f172781a3ff87"}, + {file = "pylint-2.17.7.tar.gz", hash = "sha256:f4fcac7ae74cfe36bc8451e931d8438e4a476c20314b1101c458ad0f05191fad"}, +] + +[package.dependencies] +astroid = ">=2.15.8,<=2.17.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = {version = ">=0.3.6", markers = "python_version >= \"3.11\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pyobjc-core" +version = "9.2" +description = "Python<->ObjC Interoperability Module" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyobjc-core-9.2.tar.gz", hash = "sha256:d734b9291fec91ff4e3ae38b9c6839debf02b79c07314476e87da8e90b2c68c3"}, + {file = "pyobjc_core-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fa674a39949f5cde8e5c7bbcd24496446bfc67592b028aedbec7f81dc5fc4daa"}, + {file = "pyobjc_core-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bbc8de304ee322a1ee530b4d2daca135a49b4a49aa3cedc6b2c26c43885f4842"}, + {file = "pyobjc_core-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0fa950f092673883b8bd28bc18397415cabb457bf410920762109b411789ade9"}, + {file = "pyobjc_core-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:586e4cae966282eaa61b21cae66ccdcee9d69c036979def26eebdc08ddebe20f"}, + {file = "pyobjc_core-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41189c2c680931c0395a55691763c481fc681f454f21bb4f1644f98c24a45954"}, + {file = "pyobjc_core-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:2d23ee539f2ba5e9f5653d75a13f575c7e36586fc0086792739e69e4c2617eda"}, + {file = "pyobjc_core-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b9809cf96678797acb72a758f34932fe8e2602d5ab7abec15c5ac68ddb481720"}, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "9.2" +description = "Wrappers for the Cocoa frameworks on macOS" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyobjc-framework-Cocoa-9.2.tar.gz", hash = "sha256:efd78080872d8c8de6c2b97e0e4eac99d6203a5d1637aa135d071d464eb2db53"}, + {file = "pyobjc_framework_Cocoa-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9e02d8a7cc4eb7685377c50ba4f17345701acf4c05b1e7480d421bff9e2f62a4"}, + {file = "pyobjc_framework_Cocoa-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3b1e6287b3149e4c6679cdbccd8e9ef6557a4e492a892e80a77df143f40026d2"}, + {file = "pyobjc_framework_Cocoa-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:312977ce2e3989073c6b324c69ba24283de206fe7acd6dbbbaf3e29238a22537"}, + {file = "pyobjc_framework_Cocoa-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aae7841cf40c26dd915f4dd828f91c6616e6b7998630b72e704750c09e00f334"}, + {file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:739a421e14382a46cbeb9a883f192dceff368ad28ec34d895c48c0ad34cf2c1d"}, + {file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:32d9ac1033fac1b821ddee8c68f972a7074ad8c50bec0bea9a719034c1c2fb94"}, + {file = "pyobjc_framework_Cocoa-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b236bb965e41aeb2e215d4e98a5a230d4b63252c6d26e00924ea2e69540a59d6"}, +] + +[package.dependencies] +pyobjc-core = ">=9.2" + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "9.2" +description = "Wrappers for the framework CoreBluetooth on macOS" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyobjc-framework-CoreBluetooth-9.2.tar.gz", hash = "sha256:cb2481b1dfe211ae9ce55f36537dc8155dbf0dc8ff26e0bc2e13f7afb0a291d1"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:53d888742119d0f0c725d0b0c2389f68e8f21f0cba6d6aec288c53260a0196b6"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:179532882126526e38fe716a50fb0ee8f440e0b838d290252c515e622b5d0e49"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:256a5031ea9d8a7406541fa1b0dfac549b1de93deae8284605f9355b13fb58be"}, +] + +[package.dependencies] +pyobjc-core = ">=9.2" +pyobjc-framework-Cocoa = ">=9.2" + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "9.2" +description = "Wrappers for libdispatch on macOS" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyobjc-framework-libdispatch-9.2.tar.gz", hash = "sha256:542e7f7c2b041939db5ed6f3119c1d67d73ec14a996278b92485f8513039c168"}, + {file = "pyobjc_framework_libdispatch-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88d4091d4bcb5702783d6e86b4107db973425a17d1de491543f56bd348909b60"}, + {file = "pyobjc_framework_libdispatch-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a67b007113328538b57893cc7829a722270764cdbeae6d5e1460a1d911314df"}, + {file = "pyobjc_framework_libdispatch-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6fccea1a57436cf1ac50d9ebc6e3e725bcf77f829ba6b118e62e6ed7866d359d"}, + {file = "pyobjc_framework_libdispatch-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6eba747b7ad91b0463265a7aee59235bb051fb97687f35ca2233690369b5e4e4"}, + {file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2e835495860d04f63c2d2f73ae3dd79da4222864c107096dc0f99e8382700026"}, + {file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1b107e5c3580b09553030961ea6b17abad4a5132101eab1af3ad2cb36d0f08bb"}, + {file = "pyobjc_framework_libdispatch-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:83cdb672acf722717b5ecf004768f215f02ac02d7f7f2a9703da6e921ab02222"}, +] + +[package.dependencies] +pyobjc-core = ">=9.2" + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.2" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-asyncio-0.23.2.tar.gz", hash = "sha256:c16052382554c7b22d48782ab3438d5b10f8cf7a4bdcae7f0f67f097d95beecc"}, + {file = "pytest_asyncio-0.23.2-py3-none-any.whl", hash = "sha256:ea9021364e32d58f0be43b91c6233fb8d2224ccef2398d6837559e587682808f"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-mock" +version = "3.12.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[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 = "requirementslib" +version = "3.0.0" +description = "A tool for converting between pip-style and pipfile requirements." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "requirementslib-3.0.0-py2.py3-none-any.whl", hash = "sha256:67b42903d7c32f89c7047d1020c619d37cb515c475a4ae6f4e5683e1c56d7bf7"}, + {file = "requirementslib-3.0.0.tar.gz", hash = "sha256:28f8e0b1c38b34ae06de68ef115b03bbcdcdb99f9e9393333ff06ded443e3f24"}, +] + +[package.dependencies] +distlib = ">=0.2.8" +pep517 = ">=0.5.0" +pip = ">=23.1" +platformdirs = "*" +plette = {version = "*", extras = ["validation"]} +pydantic = "*" +requests = "*" +setuptools = ">=40.8" +tomlkit = ">=0.5.3" + +[package.extras] +dev = ["nox", "parver", "towncrier", "twine"] +docs = ["sphinx", "sphinx-rtd-theme"] +tests = ["coverage", "hypothesis", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "readme-renderer[md]"] + +[[package]] +name = "setuptools" +version = "69.0.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, + {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "thelogrus" +version = "0.7.0" +description = "The Logrus is a collection of random utility functions" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "thelogrus-0.7.0-py3-none-any.whl", hash = "sha256:d6b3e16e9a6c7ef8d9b9e5823434aab2720d5d7bb83450f69aaa589b9c68eda3"}, + {file = "thelogrus-0.7.0.tar.gz", hash = "sha256:fd7171e065bb739ab54ac3c5098e601037ca7ca91366b8e96140e078b1fe4e04"}, +] + +[package.dependencies] +dateutils = ">=0.6.12,<0.7.0" +pyaml = ">=21.10.1,<22.0.0" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.12.3" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, +] + +[[package]] +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + +[[package]] +name = "urllib3" +version = "2.1.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "winrt-runtime" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +category = "main" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-runtime-2.0.0b1.tar.gz", hash = "sha256:28db2ebe7bfb347d110224e9f23fe8079cea45af0fcbd643d039524ced07d22c"}, + {file = "winrt_runtime-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:8f812b01e2c8dd3ca68aa51a7aa02e815cc2ac3c8520a883b4ec7a4fc63afb04"}, + {file = "winrt_runtime-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:f36f6102f9b7a08d917a6809117c085639b66be2c579f4089d3fd47b83e8f87b"}, + {file = "winrt_runtime-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:4a99f267da96edc977623355b816b46c1344c66dc34732857084417d8cf9a96b"}, + {file = "winrt_runtime-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:ba998e3fc452338c5e2d7bf5174a6206580245066d60079ee4130082d0eb61c2"}, + {file = "winrt_runtime-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:e7838f0fdf5653ce245888590214177a1f54884cece2c8dfbfe3d01b2780171e"}, + {file = "winrt_runtime-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:2afa45b7385e99a63d55ccda29096e6a84fcd4c654479005c147b0e65e274abf"}, + {file = "winrt_runtime-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:edda124ff965cec3a6bfdb26fbe88e004f96975dd84115176e30c1efbcb16f4c"}, + {file = "winrt_runtime-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:d8935951efeec6b3d546dce8f48bb203aface57a1ba991c066f0e12e84c8f91e"}, + {file = "winrt_runtime-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:509fb9a03af5e1125433f58522725716ceef040050d33625460b5a5eb98a46ac"}, + {file = "winrt_runtime-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:41138fe4642345d7143e817ce0905d82e60b3832558143e0a17bfea8654c6512"}, + {file = "winrt_runtime-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:081a429fe85c33cb6610c4a799184b7650b30f15ab1d89866f2bda246d3a5c0a"}, + {file = "winrt_runtime-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:e6984604c6ae1f3258973ba2503d1ea5aa15e536ca41d6a131ad305ebbb6519d"}, +] + +[[package]] +name = "winrt-windows-devices-bluetooth" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +category = "main" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Bluetooth-2.0.0b1.tar.gz", hash = "sha256:786bd43786b873a083b89debece538974f720584662a2573d6a8a8501a532860"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:79631bf3f96954da260859df9228a028835ffade0d885ba3942c5a86a853d150"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:cd85337a95065d0d2045c06db1a5edd4a447aad47cf7027818f6fb69f831c56c"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:6a963869ed003d260e90e9bedc334129303f263f068ea1c0d994df53317db2bc"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:7c5951943a3911d94a8da190f4355dc70128d7d7f696209316372c834b34d462"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:b0bb154ae92235649ed234982f609c490a467d5049c27d63397be9abbb00730e"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:6688dfb0fc3b7dc517bf8cf40ae00544a50b4dec91470d37be38fc33c4523632"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:613c6ff4125df46189b3bef6d3110d94ec725d357ab734f00eedb11c4116c367"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:59c403b64e9f4e417599c6f6aea6ee6fac960597c21eac6b3fd8a84f64aa387c"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:b7f6e1b9bb6e33be80045adebd252cf25cd648759fad6e86c61a393ddd709f7f"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:eae7a89106eab047e96843e28c3c6ce0886dd7dee60180a1010498925e9503f9"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:8dfd1915c894ac19dd0b24aba38ef676c92c3473c0d9826762ba9616ad7df68b"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:49058587e6d82ba33da0767b97a378ddfea8e3a5991bdeff680faa287bfae57e"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Enumeration[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Radios[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Networking[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-devices-bluetooth-advertisement" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +category = "main" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Bluetooth.Advertisement-2.0.0b1.tar.gz", hash = "sha256:d9050faa4377d410d4f0e9cabb5ec555a267531c9747370555ac9ec93ec9f399"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:ac9b703d16adc87c3541585525b8fcf6d84391e2fa010c2f001e714c405cc3b7"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:593cade7853a8b0770e8ef30462b5d5f477b82e17e0aa590094b1c26efd3e05a"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:574698c08895e2cfee7379bdf34a5f319fe440d7dfcc7bc9858f457c08e9712c"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:652a096f8210036bbb539d7f971eaf1f472a3aeb60b7e31278e3d0d30a355292"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:e5cfb866c44dad644fb44b441f4fdbddafc9564075f1f68f756e20f438105c67"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:6c2503eaaf5cd988b5510b86347dba45ad6ee52656f9656a1a97abae6d35386e"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:780c766725a55f4211f921c773c92c2331803e70f65d6ad6676a60f903d39a54"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:39c8633d01039eb2c2f6f20cfc43c045a333b9f3a45229e2ce443f71bb2a562c"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:eaa0d44b4158b16937eac8102249e792f0299dbb0aefc56cc9adc9552e8f9afe"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:d171487e23f7671ad2923544bfa6545d0a29a1a9ae1f5c1d5e5e5f473a5d62b2"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:442eecac87653a03617e65bdb2ef79ddc0582dfdacc2be8af841fba541577f8b"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:b30ab9b8c1ecf818be08bac86bee425ef40f75060c4011d4e6c2e624a7b9916e"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-devices-bluetooth-genericattributeprofile" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +category = "main" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1.tar.gz", hash = "sha256:93b745d51ecfb3e9d3a21623165cc065735c9e0146cb7a26744182c164e63e14"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:db740aaedd80cca5b1a390663b26c7733eb08f4c57ade6a04b055d548e9d042b"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:7c81aa6c066cdab58bcc539731f208960e094a6d48b59118898e1e804dbbdf7f"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:92277a6bbcbe2225ad1be92968af597dc77bc37a63cd729690d2d9fb5094ae25"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:6b48209669c1e214165530793cf9916ae44a0ae2618a9be7a489e8c94f7e745f"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:2f17216e6ce748eaef02fb0658213515d3ff31e2dbb18f070a614876f818c90d"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:db798a0f0762e390da5a9f02f822daff00692bd951a492224bf46782713b2938"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:b8d9dba04b9cfa53971c35117fc3c68c94bfa5e2ed18ce680f731743598bf246"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:e5260b3f33dee8a896604297e05efc04d04298329c205a74ded8e2d6333e84b7"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:822ef539389ecb546004345c4dce8b9b7788e2e99a1d6f0947a4b123dceb7fed"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:11e6863e7a94d2b6dd76ddcd19c01e311895810a4ce6ad08c7b5534294753243"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:20de8d04c301c406362c93e78d41912aea0af23c4b430704aba329420d7c2cdf"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:918059796f2f123216163b928ecde8ecec17994fb7a94042af07fda82c132a6d"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Enumeration[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-devices-enumeration" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +category = "main" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Enumeration-2.0.0b1.tar.gz", hash = "sha256:8f214040e4edbe57c4943488887db89f4a00d028c34169aafd2205e228026100"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:dcb9e7d230aefec8531a46d393ecb1063b9d4b97c9f3ff2fc537ce22bdfa2444"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:22a3e1fef40786cc8d51320b6f11ff25de6c674475f3ba608a46915e1dadf0f5"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:2edcfeb70a71d40622873cad96982a28e92a7ee71f33968212dd3598b2d8d469"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:ce4eb88add7f5946d2666761a97a3bb04cac2a061d264f03229c1e15dbd7ce91"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:a9001f17991572abdddab7ab074e08046e74e05eeeaf3b2b01b8b47d2879b64c"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:0440b91ce144111e207f084cec6b1277162ef2df452d321951e989ce87dc9ced"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:e4fae13126f13a8d9420b74fb5a5ff6a6b2f91f7718c4be2d4a8dc1337c58f59"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:e352eebc23dc94fb79e67a056c057fb0e16c20c8cb881dc826094c20ed4791e3"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:b43f5c1f053a170e6e4b44ba69838ac223f9051adca1a56506d4c46e98d1485f"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:ed245fad8de6a134d5c3a630204e7f8238aa944a40388005bce0ce3718c410fa"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:22a9eefdbfe520778512266d0b48ff239eaa8d272fce6f5cb1ff352bed0619f4"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:397d43f8fd2621a7719b9eab6a4a8e72a1d6fa2d9c36525a30812f8e7bad3bdf"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.ApplicationModel.Background[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Security.Credentials[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)", "winrt-Windows.UI.Popups[all] (==2.0.0-beta.1)", "winrt-Windows.UI[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-foundation" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +category = "main" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Foundation-2.0.0b1.tar.gz", hash = "sha256:976b6da942747a7ca5a179a35729d8dc163f833e03b085cf940332a5e9070d54"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:5337ac1ec260132fbff868603e73a3738d4001911226e72669b3d69c8a256d5e"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:af969e5bb9e2e41e4e86a361802528eafb5eb8fe87ec1dba6048c0702d63caa8"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:bbbfa6b3c444a1074a630fd4a1b71171be7a5c9bb07c827ad9259fadaed56cf2"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:b91bd92b1854c073acd81aa87cf8df571d2151b1dd050b6181aa36f7acc43df4"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:2f5359f25703347e827dbac982150354069030f1deecd616f7ce37ad90cbcb00"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:0f1f1978173ddf0ee6262c2edb458f62d628b9fa0df10cd1e8c78c833af3197e"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:c1d23b737f733104b91c89c507b58d0b3ef5f3234a1b608ef6dfb6dbbb8777ea"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:95de6c29e9083fe63f127b965b54dfa52a6424a93a94ce87cfad4c1900a6e887"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:4707063a5a6980e3f71aebeea5ac93101c753ec13a0b47be9ea4dbc0d5ff361e"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:d0259f1f4a1b8e20d0cbd935a889c0f7234f720645590260f9cf3850fdc1e1fa"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:15c7b324d0f59839fb4492d84bb1c870881c5c67cb94ac24c664a7c4dce1c475"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:16ad741f4d38e99f8409ba5760299d0052003255f970f49f4b8ba2e0b609c8b7"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-foundation-collections" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +category = "main" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Foundation.Collections-2.0.0b1.tar.gz", hash = "sha256:185d30f8103934124544a40aac005fa5918a9a7cb3179f45e9863bb86e22ad43"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:042142e916a170778b7154498aae61254a1a94c552954266b73479479d24f01d"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:9f68e66055121fc1e04c4fda627834aceee6fbe922e77d6ccaecf9582e714c57"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:a4609411263cc7f5e93a9a5677b21e2ef130e26f9030bfa960b3e82595324298"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:5296858aa44c53936460a119794b80eedd6bd094016c1bf96822f92cb95ea419"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:3db1e1c80c97474e7c88b6052bd8982ca61723fd58ace11dc91a5522662e0b2a"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:c3a594e660c59f9fab04ae2f40bda7c809e8ec4748bada4424dfb02b43d4bfe1"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:0f355ee943ec5b835e694d97e9e93545a42d6fb984a61f442467789550d62c3f"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:c4a0cd2eb9f47c7ca3b66d12341cc822250bf26854a93fd58ab77f7a48dfab3a"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:744dbef50e8b8f34904083cae9ad43ac6e28facb9e166c4f123ce8e758141067"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:b7c767184aec3a3d7cba2cd84fadcd68106854efabef1a61092052294d6d6f4f"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:7c1ffe99c12f14fc4ab7027757780e6d850fa2fb23ec404a54311fbd9f1970d3"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:870fa040ed36066e4c240c35973d8b2e0d7c38cc6050a42d993715ec9e3b748c"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Foundation[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-storage-streams" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +category = "main" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Storage.Streams-2.0.0b1.tar.gz", hash = "sha256:029d67cdc9b092d56c682740fe3c42f267dc5d3346b5c0b12ebc03f38e7d2f1f"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:49c90d4bfd539f6676226dfcb4b3574ddd6be528ffc44aa214c55af88c2de89e"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:22cc82779cada84aa2633841e25b33f3357737d912a1d9ecc1ee5a8b799b5171"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:b1750a111be32466f4f0781cbb5df195ac940690571dff4564492b921b162563"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:e79b1183ab26d9b95cf3e6dbe3f488a40605174a5a112694dbb7dbfb50899daf"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:3e90a1207eb3076f051a7785132f7b056b37343a68e9481a50c6defb3f660099"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:4da06522b4fa9cfcc046b604cc4aa1c6a887cc4bb5b8a637ed9bff8028a860bb"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:6f74f8ab8ac0d8de61c709043315361d8ac63f8144f3098d428472baadf8246a"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:5cf7c8d67836c60392d167bfe4f98ac7abcb691bfba2d19e322d0f9181f58347"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:f7f679f2c0f71791eca835856f57942ee5245094c1840a6c34bc7c2176b1bcd6"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:5beb53429fa9a11ede56b4a7cefe28c774b352dd355f7951f2a4dd7e9ec9b39a"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:f84233c4b500279d8f5840cb8c47776bc040fcecba05c6c9ab9767053698fc8b"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:cfb163ddbb435906f75ef92a768573b0190e194e1438cea5a4c1d4d32a6b9386"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Storage[all] (==2.0.0-beta.1)", "winrt-Windows.System[all] (==2.0.0-beta.1)"] + +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + +[[package]] +name = "yarg" +version = "0.1.9" +description = "A semi hard Cornish cheese, also queries PyPI (PyPI client)" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "yarg-0.1.9-py2.py3-none-any.whl", hash = "sha256:4f9cebdc00fac946c9bf2783d634e538a71c7d280a4d806d45fd4dc0ef441492"}, + {file = "yarg-0.1.9.tar.gz", hash = "sha256:55695bf4d1e3e7f756496c36a69ba32c40d18f821e38f61d028f6049e5e15911"}, +] + +[package.dependencies] +requests = "*" + +[metadata] +lock-version = "2.0" +python-versions = ">=3.11.0,<3.13" # Bleak requires version < 3.13 +content-hash = "f990ced5905c62eeebf0a963772ae5238a98ac78e84b9666f2ab45fe38abf363" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5a304c7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[tool.poetry] +name = "plejd-mqtt-ha" +version = "0.0.1" +description = "" +authors = ["Viktor Karlquist "] +readme = "README.md" +packages = [{include = "plejd_mqtt_ha"}] + +[tool.poetry.dependencies] +python = ">=3.11.0,<3.13" # Bleak requires version < 3.13 +paho-mqtt = "^1.6.1" # MQTT library +ha-mqtt-discoverable = "^0.12.0" # Create MQTT entities that are discovered by HA +bleak = "^0.21.1" # BLE library +requests = "^2.31.0" # To communicate with plejd API +tomli = "^2.0.1" # TOML parser to store plejd site +numpy = "^1.25.2" # To parse BLE payload, could probably be done without numpy +cryptography = "^41.0.5" # Used to create AES cipher to authenticate against plejd mesh +pydantic = "^1.10.13" # Data validation + +[tool.poetry.group.dev.dependencies] +mypy = "^1.5.1" # Type checking +pylint = "^2.17.5" # Linting +pycodestyle = "^2.11.0" # Code style +pytest = "^7.4.3" # Tests + +[tool.pylint.format] +max-line-length = 100 + +[tool.flake8] +max-line-length = 100 + +[tool.black] +line-length = 100 + +[tool.pylint.'MESSAGES CONTROL'] +disable="fixme" + +[tool.pylint.MASTER] +ignore="tools" + +[tool.mypy] +warn_return_any = true +warn_unused_configs = true + +[tool.poetry.group.test.dependencies] +pytest-mock = "^3.6.1" +pytest-asyncio = "^0.23.2" + +[tool.pytest.ini_options] +testpaths = "tests" diff --git a/tests/test_bt_client.py b/tests/test_bt_client.py new file mode 100644 index 0000000..1bb7cdb --- /dev/null +++ b/tests/test_bt_client.py @@ -0,0 +1,301 @@ +import datetime +import struct + +import pytest +from plejd_mqtt_ha import constants +from plejd_mqtt_ha.bt_client import ( + BTClient, + PlejdBluetoothError, + PlejdNotConnectedError, + UnsupportedCommandError, +) +from plejd_mqtt_ha.mdl.bt_device_info import BTDeviceInfo +from plejd_mqtt_ha.mdl.settings import API, PlejdSettings + + +class TestBTClient: + """Test BTClient class""" + + @pytest.fixture(autouse=True) + def setup(self): + self.settings = PlejdSettings(api=API(user="test_user", password="test_password")) + self.bt_client = BTClient("deadbeef", self.settings) + self.bt_device = BTDeviceInfo( + category="category", + model="model", + device_id=1, + unique_id="unique_id", + hardware_id="hardware_id", + index=1, + name="device", + ble_address=1, + plejd_id=1, + device_type="type", + ) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "is_connected,_connect,expected", + [ + (True, True, True), # Already connected + (False, True, True), # Connect success + (False, False, False), # Connect failed + ], + ) + async def test_connect(self, mocker, is_connected, _connect, expected): + """Test connect method of BTClient""" + self.bt_client.is_connected = mocker.MagicMock(return_value=is_connected) + self.bt_client._connect = mocker.AsyncMock(return_value=_connect) + self.bt_client._stay_connected_loop = mocker.MagicMock() + + result = await self.bt_client.connect(stay_connected=False) + + assert result == expected + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "is_connected,_disconnect,expected", + [ + (True, True, True), # Already connected, disconnect should be successful + (False, True, True), # Not connected, disconnect should still return True + (True, False, False), # Connected, disconnect should return False + (False, False, True), # Not connected, no disconnect should be attempted + ], + ) + async def test_disconnect(self, mocker, is_connected, _disconnect, expected): + """Test disconnect method of BTClient""" + self.bt_client.is_connected = mocker.MagicMock(return_value=is_connected) + self.bt_client._client = mocker.MagicMock() + self.bt_client._client.disconnect = mocker.AsyncMock(return_value=_disconnect) + + result = await self.bt_client.disconnect() + + assert result == expected + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "is_connected,command,write_request_success,expected_exception", + [ + (False, 0, True, PlejdNotConnectedError), # Not connected + (True, -1, True, UnsupportedCommandError), # Invalid command + ( + True, + constants.PlejdCommand.BLE_CMD_STATE_CHANGE, + False, + PlejdBluetoothError, + ), # Write request failed + ( + True, + constants.PlejdCommand.BLE_CMD_STATE_CHANGE, + True, + None, + ), # Successful case + ], + ) + async def test_send_command( + self, mocker, is_connected, command, write_request_success, expected_exception + ): + """Test send_command method of BTClient""" + self.bt_client.is_connected = mocker.MagicMock(return_value=is_connected) + self.bt_client._get_cmd_payload = mocker.MagicMock(return_value="payload") + self.bt_client._client = mocker.MagicMock(address="address") + self.bt_client._encode_address = mocker.MagicMock(return_value="encoded_address") + self.bt_client._encrypt_decrypt_data = mocker.MagicMock(return_value="encrypted_data") + self.bt_client._write_request = mocker.AsyncMock( + side_effect=PlejdBluetoothError("Write request failed") + if not write_request_success + else None + ) + + if expected_exception: + with pytest.raises(expected_exception): + await self.bt_client.send_command(1, command, "data", 1) + else: + await self.bt_client.send_command(1, command, "data", 1) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "write_request_success, read_request_success, pong_data, expected", + [ + (False, True, b"\x01", False), # Write request failed + (True, False, b"\x01", False), # Read request failed + (True, True, b"", False), # Pong data is empty + (True, True, b"\x02", False), # Pong data is not the expected value + (True, True, b"\x01", True), # Successful case + ], + ) + async def test_ping( + self, mocker, write_request_success, read_request_success, pong_data, expected + ): + """Test ping method of BTClient""" + ping_data = b"\x00" + mocker.patch("plejd_mqtt_ha.bt_client.randbytes", return_value=ping_data) + self.bt_client._write_request = mocker.AsyncMock( + side_effect=PlejdBluetoothError("Write request failed") + if not write_request_success + else None + ) + + self.bt_client._read_request = mocker.AsyncMock( + side_effect=PlejdBluetoothError("Read request failed") + if not read_request_success + else None, + return_value=pong_data, + ) + result = await self.bt_client.ping() + + assert result == expected + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "is_connected, read_request_success, encrypted_data, expected_exception", + [ + (False, True, b"\x01", PlejdNotConnectedError), # Not connected + (True, False, b"\x01", PlejdBluetoothError), # _read_request failed + (True, True, b"\x01", None), # Successful case + ], + ) + async def test_get_last_data( + self, + mocker, + is_connected, + read_request_success, + encrypted_data, + expected_exception, + ): + """Test get_last_data method of BTClient""" + self.bt_client.is_connected = mocker.MagicMock(return_value=is_connected) + self.bt_client._client = mocker.MagicMock(address="address") + self.bt_client._encode_address = mocker.MagicMock(return_value="encoded_address") + self.bt_client._encrypt_decrypt_data = mocker.MagicMock(return_value="decrypted_data") + self.bt_client._read_request = mocker.AsyncMock( + side_effect=PlejdBluetoothError("Read request failed") + if not read_request_success + else None, + return_value=encrypted_data, + ) + + if expected_exception: + with pytest.raises(expected_exception): + await self.bt_client.get_last_data() + else: + result = await self.bt_client.get_last_data() + assert result == "decrypted_data" + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "is_connected," + "send_command_success," + "get_last_data_success," + "last_data," + "expected_exception," + "expected_result", + [ + ( + False, + True, + True, + b"\x01\x02\x03\x04\x05\x06\x07\x08", + PlejdNotConnectedError, + None, + ), # Not connected + ( + True, + False, + True, + b"\x01\x02\x03\x04\x05\x06\x07\x08", + PlejdBluetoothError, + None, + ), # send_command failed + ( + True, + True, + False, + b"\x01\x02\x03\x04\x05\x06\x07\x08", + PlejdBluetoothError, + None, + ), # get_last_data failed + ( + True, + True, + True, + b"\x01\x02\x03\x04\x05\x06\x07\x08", + UnsupportedCommandError, + None, + ), # Invalid last_data + ( + True, + True, + True, + b"\x01\x02\x00\x00\x1b\x00\x00\x00\x00\x00\x00\x00", + None, + datetime.datetime.fromtimestamp(0), + ), # Successful case + ], + ) + async def test_get_plejd_time( + self, + mocker, + is_connected, + send_command_success, + get_last_data_success, + last_data, + expected_exception, + expected_result, + ): + """Test get_plejd_time method of BTClient""" + + self.bt_client.is_connected = mocker.MagicMock(return_value=is_connected) + self.bt_client.send_command = mocker.AsyncMock( + side_effect=PlejdBluetoothError("send_command failed") + if not send_command_success + else None + ) + self.bt_client.get_last_data = mocker.AsyncMock( + side_effect=PlejdBluetoothError("get_last_data failed") + if not get_last_data_success + else None, + return_value=last_data, + ) + + if expected_exception: + with pytest.raises(expected_exception): + await self.bt_client.get_plejd_time(self.bt_device) + else: + result = await self.bt_client.get_plejd_time(self.bt_device) + assert result == expected_result + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "send_command_success, expected_exception, expected_result", + [ + (False, PlejdNotConnectedError, False), # Not connected + (False, PlejdBluetoothError, False), # send_command failed + (True, None, True), # Successful case + ], + ) + async def test_set_plejd_time( + self, mocker, send_command_success, expected_exception, expected_result + ): + """Test set_plejd_time method of BTClient""" + + time = datetime.datetime.now() + timestamp = struct.pack("= 400: + mock_response.raise_for_status.side_effect = requests.HTTPError() + mocker.patch("requests.post", return_value=mock_response) + + api = PlejdAPI(self.settings) + if expected == IncorrectCredentialsError: + with pytest.raises(expected): + api.login() + elif expected == UnknownResponseError: + with pytest.raises(expected): + api.login() + elif expected == PlejdAPIError: + with pytest.raises(PlejdAPIError): + api.login() + else: + api.login() + assert api._session_token == expected + + # TODO add positive test for get_site + + @pytest.mark.parametrize( + "exception", + [requests.RequestException, ValueError, UnknownResponseError("")], + ) + def test_get_site_exceptions(self, mocker, exception): + """Test get_site method of PlejdAPI when an exception is raised""" + api = PlejdAPI(self.settings) + api._session_token = "test_token" + + mocker.patch("requests.post", side_effect=exception) + with pytest.raises(PlejdAPIError): + api.get_site()