pax_global_header00006660000000000000000000000064150106552750014520gustar00rootroot0000000000000052 comment=148a6faa5c37ce619e5749da78be507e50e2b890 python-roborock-2.19.0/000077500000000000000000000000001501065527500147505ustar00rootroot00000000000000python-roborock-2.19.0/.github/000077500000000000000000000000001501065527500163105ustar00rootroot00000000000000python-roborock-2.19.0/.github/dependabot.yml000066400000000000000000000005351501065527500211430ustar00rootroot00000000000000# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" python-roborock-2.19.0/.github/workflows/000077500000000000000000000000001501065527500203455ustar00rootroot00000000000000python-roborock-2.19.0/.github/workflows/ci.yml000066400000000000000000000045201501065527500214640ustar00rootroot00000000000000name: CI on: push: branches: - main pull_request: concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: # Make sure commit messages follow the conventional commits convention: # https://www.conventionalcommits.org commitlint: name: Lint Commit Messages runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v6.2.1 lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" - uses: pre-commit/action@v3.0.1 test: strategy: fail-fast: false matrix: python-version: - "3.11" - "3.12" - "3.13" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - uses: snok/install-poetry@v1.4.1 - name: Install Dependencies run: poetry install shell: bash - name: Test with Pytest run: poetry run pytest shell: bash release: runs-on: ubuntu-latest needs: - test concurrency: release if: github.ref == 'refs/heads/main' permissions: contents: write issues: write pull-requests: write id-token: write actions: write packages: write environment: name: release steps: - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - name: Python Semantic Release id: release uses: python-semantic-release/python-semantic-release@v9.21.0 with: github_token: ${{ secrets.GH_TOKEN }} - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@v1.12.4 # NOTE: DO NOT wrap the conditional in ${{ }} as it will always evaluate to true. # See https://github.com/actions/runner/issues/1173 if: steps.release.outputs.released == 'true' - name: Publish package distributions to GitHub Releases uses: python-semantic-release/upload-to-gh-release@v9.8.9 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GH_TOKEN }} python-roborock-2.19.0/.gitignore000066400000000000000000000002251501065527500167370ustar00rootroot00000000000000dist venv .venv .idea roborock/__pycache__ *.pyc .coverage # Sphinx documentation docs/_build/ # mkdocs documentation /site /docs/build/ .DS_Store python-roborock-2.19.0/.pre-commit-config.yaml000066400000000000000000000024201501065527500212270ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks exclude: "CHANGELOG.md" default_stages: [ commit ] repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: debug-statements - id: check-builtin-literals - id: check-case-conflict - id: check-docstring-first - id: check-json - id: check-toml - id: check-yaml - id: detect-private-key - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-poetry/poetry rev: 1.7.1 hooks: - id: poetry-check - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: - id: codespell - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.1.8 hooks: - id: ruff-format - id: ruff args: - --fix - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.7.1 hooks: - id: mypy exclude: cli.py additional_dependencies: [ "types-paho-mqtt" ] python-roborock-2.19.0/.readthedocs.yaml000066400000000000000000000002001501065527500201670ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.10" python: install: - requirements: docs/requirements.txt python-roborock-2.19.0/.vscode/000077500000000000000000000000001501065527500163115ustar00rootroot00000000000000python-roborock-2.19.0/.vscode/launch.json000066400000000000000000000004501501065527500204550ustar00rootroot00000000000000{ "version": "0.2.0", "configurations": [ { "name": "Python: Current File", "type": "python", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": false } ] } python-roborock-2.19.0/.vscode/settings.json000066400000000000000000000000451501065527500210430ustar00rootroot00000000000000{ "esbonio.sphinx.confDir": "" } python-roborock-2.19.0/CHANGELOG.md000066400000000000000000002736401501065527500165750ustar00rootroot00000000000000# CHANGELOG ## v2.19.0 (2025-05-13) ### Bug Fixes - Add Saros 10 dock type code ([#362](https://github.com/Python-roborock/python-roborock/pull/362), [`240bf59`](https://github.com/Python-roborock/python-roborock/commit/240bf59df1873e85e05356496e5be01f1a000199)) ### Chores - **deps**: Bump aiomqtt from 2.3.2 to 2.4.0 ([#375](https://github.com/Python-roborock/python-roborock/pull/375), [`b243a25`](https://github.com/Python-roborock/python-roborock/commit/b243a25569c2cb6b54e6c0e1eed6dadecb9ad84c)) Bumps [aiomqtt](https://github.com/empicano/aiomqtt) from 2.3.2 to 2.4.0. - [Release notes](https://github.com/empicano/aiomqtt/releases) - [Changelog](https://github.com/empicano/aiomqtt/blob/main/CHANGELOG.md) - [Commits](https://github.com/empicano/aiomqtt/compare/v2.3.2...v2.4.0) --- updated-dependencies: - dependency-name: aiomqtt dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ### Features - Add some logging for the web api ([#377](https://github.com/Python-roborock/python-roborock/pull/377), [`74c1b5f`](https://github.com/Python-roborock/python-roborock/commit/74c1b5f6e88ce410f95676de802bd04d304963b1)) ## v2.18.2 (2025-05-04) ### Bug Fixes - Add session to home_data_v3 ([#372](https://github.com/Python-roborock/python-roborock/pull/372), [`77061fe`](https://github.com/Python-roborock/python-roborock/commit/77061fe1545a3d2f9e874a3f7e4a94eedfd17706)) ## v2.18.1 (2025-05-04) ### Bug Fixes - Get home_data_v3 working ([#371](https://github.com/Python-roborock/python-roborock/pull/371), [`f9e6c54`](https://github.com/Python-roborock/python-roborock/commit/f9e6c546e68a71a321dafabd5d502abef3e89b31)) ## v2.18.0 (2025-04-06) ### Features - Rate limits for login and home data ([#361](https://github.com/Python-roborock/python-roborock/pull/361), [`93ef8ad`](https://github.com/Python-roborock/python-roborock/commit/93ef8addfd2faa6264606c9d710c46772cd52150)) * feat: rate limits for login and home data * fix: comments * fix: testing and comments ## v2.17.0 (2025-04-05) ### Features - Add support for g20s ultra ([#359](https://github.com/Python-roborock/python-roborock/pull/359), [`593c368`](https://github.com/Python-roborock/python-roborock/commit/593c3687064779ee6790e17f40411cd8129b756e)) ## v2.16.1 (2025-03-22) ### Bug Fixes - Close the session if we created it ([#356](https://github.com/Python-roborock/python-roborock/pull/356), [`96cc718`](https://github.com/Python-roborock/python-roborock/commit/96cc718dbd4106fa344172e2dbf0c3779344ba04)) ## v2.16.0 (2025-03-22) ### Features - Allow forcing of updating cache variables ([#355](https://github.com/Python-roborock/python-roborock/pull/355), [`eae7803`](https://github.com/Python-roborock/python-roborock/commit/eae7803db8973870c396ce45341e5d38cbfaf321)) ## v2.15.0 (2025-03-18) ### Chores - Fix documentation links ([#348](https://github.com/Python-roborock/python-roborock/pull/348), [`404a47c`](https://github.com/Python-roborock/python-roborock/commit/404a47c8c51891ed90093869e567d56386cdc4a2)) ### Features - Allow passing in clientsession ([#354](https://github.com/Python-roborock/python-roborock/pull/354), [`1d31cf6`](https://github.com/Python-roborock/python-roborock/commit/1d31cf619ef38dfdd2891cd42c0acf4550b88c29)) * feat: allow passing in clientsession * fix: test ## v2.14.0 (2025-03-16) ### Features - Add load_multi_map function ([#349](https://github.com/Python-roborock/python-roborock/pull/349), [`23bae12`](https://github.com/Python-roborock/python-roborock/commit/23bae1225389b6ec88bad868b8c6d4a28f458e61)) ## v2.13.0 (2025-03-16) ### Features - Add home_data_v3 ([#347](https://github.com/Python-roborock/python-roborock/pull/347), [`1325fda`](https://github.com/Python-roborock/python-roborock/commit/1325fdaef0f9d920ab499a0550da51cdb8efc0c4)) * feat: add home_data_v3 * fix: address comments ## v2.12.2 (2025-03-11) ### Bug Fixes - Bad dock summary logic ([#345](https://github.com/Python-roborock/python-roborock/pull/345), [`eda1e98`](https://github.com/Python-roborock/python-roborock/commit/eda1e98e5ea177e2eb2390d877b383780f938fd8)) ### Chores - **deps-dev**: Bump pytest from 8.3.4 to 8.3.5 ([#342](https://github.com/Python-roborock/python-roborock/pull/342), [`53635ed`](https://github.com/Python-roborock/python-roborock/commit/53635eda2a2415fc5744f9ebdf8e80fb2df96ef0)) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.4 to 8.3.5. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.4...8.3.5) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump ruff from 0.9.9 to 0.9.10 ([#344](https://github.com/Python-roborock/python-roborock/pull/344), [`94b281d`](https://github.com/Python-roborock/python-roborock/commit/94b281daf5906ec572fa679869eb78fab030db59)) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.9 to 0.9.10. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.9...0.9.10) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ## v2.12.1 (2025-03-04) ### Bug Fixes - Add error for web calls and saros dock ([#343](https://github.com/Python-roborock/python-roborock/pull/343), [`49fb137`](https://github.com/Python-roborock/python-roborock/commit/49fb1372aead96ad5b03222699ab150bf83b31f9)) ### Chores - **deps**: Bump aiohttp from 3.11.11 to 3.11.12 ([#328](https://github.com/Python-roborock/python-roborock/pull/328), [`f2d0c39`](https://github.com/Python-roborock/python-roborock/commit/f2d0c39353aff0d2f63ba5402cbfd1fd5c9f70c3)) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump aiohttp from 3.11.12 to 3.11.13 ([#340](https://github.com/Python-roborock/python-roborock/pull/340), [`7c6bb54`](https://github.com/Python-roborock/python-roborock/commit/7c6bb544fe14b0512eb4cc73f3d92f19fc56f4f7)) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump python-semantic-release/python-semantic-release ([#338](https://github.com/Python-roborock/python-roborock/pull/338), [`15f7705`](https://github.com/Python-roborock/python-roborock/commit/15f77056b8f2c4dcd2772812c6c2f9647f808bcd)) Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.17.0 to 9.21.0. - [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) - [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.rst) - [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.17.0...v9.21.0) --- updated-dependencies: - dependency-name: python-semantic-release/python-semantic-release dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump mypy from 1.14.1 to 1.15.0 ([#329](https://github.com/Python-roborock/python-roborock/pull/329), [`2105cdf`](https://github.com/Python-roborock/python-roborock/commit/2105cdf2a29a1ad1c1c9117e3dff4c4548466d4f)) Bumps [mypy](https://github.com/python/mypy) from 1.14.1 to 1.15.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.14.1...v1.15.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump ruff from 0.9.4 to 0.9.9 ([#341](https://github.com/Python-roborock/python-roborock/pull/341), [`4e80f7a`](https://github.com/Python-roborock/python-roborock/commit/4e80f7a86764240729982de3336173231fac6a08)) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.4 to 0.9.9. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.4...0.9.9) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ## v2.12.0 (2025-02-21) ### Features - Add cli status ([#333](https://github.com/Python-roborock/python-roborock/pull/333), [`64e77d7`](https://github.com/Python-roborock/python-roborock/commit/64e77d7150babcc78ce3698fe98594891dcb7bd4)) ## v2.11.3 (2025-02-19) ### Bug Fixes - Q revo curv mappings ([#332](https://github.com/Python-roborock/python-roborock/pull/332), [`83d010a`](https://github.com/Python-roborock/python-roborock/commit/83d010acbc100f06ae322adde1eedcfd0f78efc8)) ## v2.11.2 (2025-02-13) ### Bug Fixes - Add some extra data protocol checking ([#331](https://github.com/Python-roborock/python-roborock/pull/331), [`4af1490`](https://github.com/Python-roborock/python-roborock/commit/4af1490ea4db0dbeb5d5666019d9433af4f3d273)) ## v2.11.1 (2025-02-03) ### Bug Fixes - Typing of scene api call ([#324](https://github.com/Python-roborock/python-roborock/pull/324), [`61e27ae`](https://github.com/Python-roborock/python-roborock/commit/61e27aedfbb363913f80ace3932fa4adf61f9792)) ## v2.11.0 (2025-02-03) ### Chores - **deps**: Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4 ([#311](https://github.com/Python-roborock/python-roborock/pull/311), [`cb40279`](https://github.com/Python-roborock/python-roborock/commit/cb4027994e4ee0b72f25d9f51f46f8b3f9522bc5)) - **deps**: Bump python-semantic-release/python-semantic-release ([#312](https://github.com/Python-roborock/python-roborock/pull/312), [`7827af5`](https://github.com/Python-roborock/python-roborock/commit/7827af5ef7e6fb2dedd6eef0cb8c0c8439d2a8ef)) - **deps**: Bump python-semantic-release/upload-to-gh-release ([#290](https://github.com/Python-roborock/python-roborock/pull/290), [`87038e3`](https://github.com/Python-roborock/python-roborock/commit/87038e3a556a359d552775195d7640b6cdbeb1fe)) - **deps**: Bump wagoid/commitlint-github-action from 6.2.0 to 6.2.1 ([#296](https://github.com/Python-roborock/python-roborock/pull/296), [`037e28c`](https://github.com/Python-roborock/python-roborock/commit/037e28c38df282dac09bd4ff9596dc0b3a09c78f)) - **deps-dev**: Bump codespell from 2.3.0 to 2.4.1 ([#321](https://github.com/Python-roborock/python-roborock/pull/321), [`c36d46f`](https://github.com/Python-roborock/python-roborock/commit/c36d46f90780db50f2c5c2e947ada78b6ee4967c)) - **deps-dev**: Bump pytest-asyncio from 0.25.2 to 0.25.3 ([#322](https://github.com/Python-roborock/python-roborock/pull/322), [`9e40fe7`](https://github.com/Python-roborock/python-roborock/commit/9e40fe780224903c8e81c4d210ab61212582948d)) - **deps-dev**: Bump ruff from 0.9.2 to 0.9.4 ([#323](https://github.com/Python-roborock/python-roborock/pull/323), [`25d15a7`](https://github.com/Python-roborock/python-roborock/commit/25d15a78d1f5ffb069159aa652c2ef3f88d3eb03)) ### Features - Add scenes/routines support ([#317](https://github.com/Python-roborock/python-roborock/pull/317), [`090d912`](https://github.com/Python-roborock/python-roborock/commit/090d912872712e16b24597826a0b85d22b37acb3)) * add scenes support --------- Co-authored-by: Luke Lashley ## v2.10.1 (2025-02-03) ### Bug Fixes - Delete in cli ([#320](https://github.com/Python-roborock/python-roborock/pull/320), [`6704f55`](https://github.com/Python-roborock/python-roborock/commit/6704f55915005d771d698e58dcbac5ec46a385e5)) ## v2.10.0 (2025-01-31) ### Features - Add commands to add a new device ([#307](https://github.com/Python-roborock/python-roborock/pull/307), [`430c248`](https://github.com/Python-roborock/python-roborock/commit/430c24806fa06a5cec6c7fb3945a9b9cbfbc2f7a)) * feat: add commands to add a new device * chore: mr comments ## v2.9.8 (2025-01-30) ### Bug Fixes - Ignore ping id during id check ([#316](https://github.com/Python-roborock/python-roborock/pull/316), [`b3d74b4`](https://github.com/Python-roborock/python-roborock/commit/b3d74b4bc9fa581da0485cf68a46c23f53fdbf50)) ## v2.9.7 (2025-01-28) ### Bug Fixes - Never create a new asyncio loop ([#310](https://github.com/Python-roborock/python-roborock/pull/310), [`ed7db1f`](https://github.com/Python-roborock/python-roborock/commit/ed7db1f09f379f509a38a61a445fb2c41b384f25)) ## v2.9.6 (2025-01-26) ### Bug Fixes - Remove the __del__ warning for disconnected clients ([#308](https://github.com/Python-roborock/python-roborock/pull/308), [`235752b`](https://github.com/Python-roborock/python-roborock/commit/235752bd77e4617323366b56439bf8981b071430)) ### Refactoring - Breaking change to remove sync APIs ([#306](https://github.com/Python-roborock/python-roborock/pull/306), [`3c30d93`](https://github.com/Python-roborock/python-roborock/commit/3c30d933f680cc567b10ad6566b02289eade5b3f)) * refactor: breaking change to remove sync APIs * chore: downgrade log to a debug message ## v2.9.5 (2025-01-21) ### Bug Fixes - Fix queue timeout variable and set default in tests of 10 seconds ([#302](https://github.com/Python-roborock/python-roborock/pull/302), [`9c75e3a`](https://github.com/Python-roborock/python-roborock/commit/9c75e3a67fc8f411c5496b5864a9a0e90a573c8a)) * test: set queue timeout of 10 * test: cleanup lint errors * fix: set queue_timeout in the client leaf base classes * chore: fix test fixture after merging - Log an explicit message when intentionally resetting the connection ([#304](https://github.com/Python-roborock/python-roborock/pull/304), [`a20d2ac`](https://github.com/Python-roborock/python-roborock/commit/a20d2ac46c7553c7b7c7dffbbc86ee0da370418d)) ## v2.9.4 (2025-01-21) ### Bug Fixes - Bump paho-mqtt from 1.6.1 to 2.1.0 ([#288](https://github.com/Python-roborock/python-roborock/pull/288), [`777b736`](https://github.com/Python-roborock/python-roborock/commit/777b736440a3633c089bf09ab9d7240e54e0fb0e)) Bumps [paho-mqtt](https://github.com/eclipse/paho.mqtt.python) from 1.6.1 to 2.1.0. - [Release notes](https://github.com/eclipse/paho.mqtt.python/releases) - [Changelog](https://github.com/eclipse-paho/paho.mqtt.python/blob/master/ChangeLog.txt) - [Commits](https://github.com/eclipse/paho.mqtt.python/compare/v1.6.1...v2.1.0) --- updated-dependencies: - dependency-name: paho-mqtt dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - Set unique sequence numbers on outgoing messages ([#300](https://github.com/Python-roborock/python-roborock/pull/300), [`14f03c7`](https://github.com/Python-roborock/python-roborock/commit/14f03c7df1c574ab87ea056227bb95f9150f4832)) ### Chores - Fix flaky tests by cleaning up threads ([#303](https://github.com/Python-roborock/python-roborock/pull/303), [`6e29e74`](https://github.com/Python-roborock/python-roborock/commit/6e29e7440f61ddde9a67b25c87864ed0cbf1a097)) * chore: set log level to debug to aid in tracking down flaky tests * test: update log format to include timestamps and dates test: update logmessage with package name chore: fix tests to use valid zeo codes * test: fix zeo test assertion * test: add logging when updating future * test: make the client read socket always available for reading to avoid getting blocked * test: revert socket changes * test: set function loop scope * test: add pytest-timeout with a 20 second hard timeout * test: explicitly disconnect threads * test: fix formatting * test: fix lint errors * fix: stop the mqtt loop on disconnect * fix: release the mqtt thread on release * test: revert log changes * chore: cleanup/revert changes * chore: revert mqtt client check * fix: always stop the event loop when disconnecting ## v2.9.3 (2025-01-21) ### Bug Fixes - Remove methods no longer available in paho-mqtt ([#298](https://github.com/Python-roborock/python-roborock/pull/298), [`685edc8`](https://github.com/Python-roborock/python-roborock/commit/685edc825fbf2062d61c3294ea82c4566442dd64)) ### Chores - Remove test that creates abstract base class ([#299](https://github.com/Python-roborock/python-roborock/pull/299), [`a55b804`](https://github.com/Python-roborock/python-roborock/commit/a55b804fddff318d704cc04e6c4190514e3e3375)) - **deps-dev**: Bump aioresponses from 0.7.7 to 0.7.8 ([#295](https://github.com/Python-roborock/python-roborock/pull/295), [`ab7ffb3`](https://github.com/Python-roborock/python-roborock/commit/ab7ffb36190090e6d5b39150da4ebe2f2e22fbd4)) Bumps [aioresponses](https://github.com/pnuckowski/aioresponses) from 0.7.7 to 0.7.8. - [Release notes](https://github.com/pnuckowski/aioresponses/releases) - [Commits](https://github.com/pnuckowski/aioresponses/compare/0.7.7...0.7.8) --- updated-dependencies: - dependency-name: aioresponses dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ## v2.9.2 (2025-01-19) ### Bug Fixes - Update local API protocol broken during refactoring and add additional tests for API calls ([#293](https://github.com/Python-roborock/python-roborock/pull/293), [`ea8e55a`](https://github.com/Python-roborock/python-roborock/commit/ea8e55a0b9c54e7c7d6235ad0e73f7b75ec4de7b)) * test: add an additional local API test and fix bug in test fixture * test: fix formatting * fix: Update local API ### Chores - Remove dacite and update readme ([#294](https://github.com/Python-roborock/python-roborock/pull/294), [`699a2c5`](https://github.com/Python-roborock/python-roborock/commit/699a2c5ed5362ee4004d2888037baf929869e98c)) - Update CI to run on one platform, but multiple python versions ([#292](https://github.com/Python-roborock/python-roborock/pull/292), [`16ab4ff`](https://github.com/Python-roborock/python-roborock/commit/16ab4ff433d25df9daa4bf102569c39bbd686420)) ## v2.9.1 (2025-01-13) ### Bug Fixes - Bump commitlint and allow caps ([#283](https://github.com/Python-roborock/python-roborock/pull/283), [`6211a81`](https://github.com/Python-roborock/python-roborock/commit/6211a8163d130c41594daf65e36be2d87788a5c6)) * fix: bump commitlint and allow caps * fix: error ### Chores - Add end-to-end tests for the MQTT client ([#278](https://github.com/Python-roborock/python-roborock/pull/278), [`0872691`](https://github.com/Python-roborock/python-roborock/commit/0872691c9eeb6e564a1ee47b8ba2bec73eb81a63)) * test: add end-to-end tests for the MQTT client * test: extract connected client to a fixture style: fix formatting of tests refactor: extract variables for mock data used in mqtt tests style: fix lint errors in tests - Add local api test coverage ([#284](https://github.com/Python-roborock/python-roborock/pull/284), [`c8dcd34`](https://github.com/Python-roborock/python-roborock/commit/c8dcd34c8197b9d47ec3c96567313d658e0f36b3)) - Allow type checking in roborock/cloud_api.py ([#280](https://github.com/Python-roborock/python-roborock/pull/280), [`9100bbf`](https://github.com/Python-roborock/python-roborock/commit/9100bbff1390a706a74dc0ec15c1bb1d7dc83d9f)) - Inheritance fixes and simplifications ([#282](https://github.com/Python-roborock/python-roborock/pull/282), [`1013cb5`](https://github.com/Python-roborock/python-roborock/commit/1013cb5f35ec6feb71e58a437395b0cdaa593937)) - Remove level of inheritance in mqtt client ([#286](https://github.com/Python-roborock/python-roborock/pull/286), [`5add0da`](https://github.com/Python-roborock/python-roborock/commit/5add0dac8d1e1e86b184ebad709034ea2a2686a3)) - Remove one level of local client inheritence ([#285](https://github.com/Python-roborock/python-roborock/pull/285), [`1f5a9ec`](https://github.com/Python-roborock/python-roborock/commit/1f5a9ecd907c0314cc156a59156b03151e9c26a8)) - Use asyncio mode in tests ([#272](https://github.com/Python-roborock/python-roborock/pull/272), [`8f779c3`](https://github.com/Python-roborock/python-roborock/commit/8f779c39b21ab429335fc5d179fe3bacc0b5d274)) - **deps**: Bump pre-commit/action from 3.0.0 to 3.0.1 ([#276](https://github.com/Python-roborock/python-roborock/pull/276), [`3f61bcc`](https://github.com/Python-roborock/python-roborock/commit/3f61bccde418c9e9e04ef059ca8a6a2dfcba8312)) - **deps**: Bump pypa/gh-action-pypi-publish from 1.12.2 to 1.12.3 ([#291](https://github.com/Python-roborock/python-roborock/pull/291), [`be52b3d`](https://github.com/Python-roborock/python-roborock/commit/be52b3d48dc7edeb164a006db10b7efe91a18b71)) - **deps-dev**: Bump pre-commit from 3.8.0 to 4.0.1 ([#287](https://github.com/Python-roborock/python-roborock/pull/287), [`f2f0c4c`](https://github.com/Python-roborock/python-roborock/commit/f2f0c4c8fa9f8fe85fd208daf28e5f7dfe02aba3)) - **deps-dev**: Bump pytest-asyncio from 0.25.1 to 0.25.2 ([#275](https://github.com/Python-roborock/python-roborock/pull/275), [`b0611f0`](https://github.com/Python-roborock/python-roborock/commit/b0611f0eb72b0078c10a5c03ae8415d21cc19c03)) - **deps-dev**: Bump ruff from 0.8.6 to 0.9.1 ([#277](https://github.com/Python-roborock/python-roborock/pull/277), [`eb8bbe3`](https://github.com/Python-roborock/python-roborock/commit/eb8bbe317b8d4f98e9c72151d6f9ca105e3c0db0)) ### Refactoring - Simplify future usage within the api clients ([#263](https://github.com/Python-roborock/python-roborock/pull/263), [`39a8661`](https://github.com/Python-roborock/python-roborock/commit/39a8661d4c5ade657cfc655a3ac78a66628bb755)) ## v2.9.0 (2025-01-09) ### Chores - Add example ([#269](https://github.com/Python-roborock/python-roborock/pull/269), [`d7a3af2`](https://github.com/Python-roborock/python-roborock/commit/d7a3af29c91bf2066f88a941789c0dc725eb7431)) - Add some testing and mocks for the web api ([#270](https://github.com/Python-roborock/python-roborock/pull/270), [`2356c16`](https://github.com/Python-roborock/python-roborock/commit/2356c16cd08cdf7210f605f9c890eb1c5631a792)) ### Features - Add dust collection mode name for typing ease ([#271](https://github.com/Python-roborock/python-roborock/pull/271), [`c85232a`](https://github.com/Python-roborock/python-roborock/commit/c85232a00b997dbc84a4b9b99b18ae1c714b7df7)) - Add product v4 and downloading code ([#267](https://github.com/Python-roborock/python-roborock/pull/267), [`b669117`](https://github.com/Python-roborock/python-roborock/commit/b6691174607a66959f4d9046dffb4cd4e782695d)) * feat: add product v4 and downloading code * fix: remove got message - Add support for qrevo curv ([#253](https://github.com/Python-roborock/python-roborock/pull/253), [`e42729a`](https://github.com/Python-roborock/python-roborock/commit/e42729aa5aedd2c77f68230825d6ce832a146f33)) * add support for qrevo curv * add dock support * revert unnecessary changes * fix: lint --------- Co-authored-by: Luke Lashley ## v2.8.5 (2025-01-06) ### Bug Fixes - Add additional log messages to track down concurrency errors ([#266](https://github.com/Python-roborock/python-roborock/pull/266), [`d750234`](https://github.com/Python-roborock/python-roborock/commit/d75023482e58689009c4df96cfc69b6080f5ada9)) - Update log message to include existing request id ([#264](https://github.com/Python-roborock/python-roborock/pull/264), [`ac8d23a`](https://github.com/Python-roborock/python-roborock/commit/ac8d23aa59342d9ae9f7c5d7c857de353e288ffa)) * fix: Update log message to include existing request id * fix: Add protocol to log message ### Chores - Always use time.monotonic ([#265](https://github.com/Python-roborock/python-roborock/pull/265), [`e14802c`](https://github.com/Python-roborock/python-roborock/commit/e14802cadde404d548cdff0c6b5906740a7e8c00)) ## v2.8.4 (2024-12-20) ### Bug Fixes - Update mop intensity, fan speed, and dock mappings for the QRevo Master ([#260](https://github.com/Python-roborock/python-roborock/pull/260), [`77f6d6f`](https://github.com/Python-roborock/python-roborock/commit/77f6d6fc917831f1966d2138bc7355292fa1e5e2)) * fix: update mop intensity, fan speed, and dock mappings for QRevo Master * Fix sorting of imports * Rerun precommit ## v2.8.3 (2024-12-19) ### Bug Fixes - Add support for QRevo Master mop mode ([#259](https://github.com/Python-roborock/python-roborock/pull/259), [`db11c0f`](https://github.com/Python-roborock/python-roborock/commit/db11c0f8ca7c08d2f795f77f7a652db4bfaa91ae)) ## v2.8.2 (2024-12-19) ### Bug Fixes - Add a mop mode to QRevoMaster ([#258](https://github.com/Python-roborock/python-roborock/pull/258), [`bf0feb7`](https://github.com/Python-roborock/python-roborock/commit/bf0feb7ee8bc9933232e8235e6efa92a451ee19e)) ## v2.8.1 (2024-12-18) ### Bug Fixes - Add config github actions ([#247](https://github.com/Python-roborock/python-roborock/pull/247), [`35f888c`](https://github.com/Python-roborock/python-roborock/commit/35f888c653ad3d41ca40d27a5ea7041df47b6bbe)) * fix: add config github actions * fix: remove placeholders - Add gh_token to checkout ([#245](https://github.com/Python-roborock/python-roborock/pull/245), [`ab9fcfe`](https://github.com/Python-roborock/python-roborock/commit/ab9fcfe4526314b09c8fd382527c5b9d9b011315)) - Bad indentation ([#248](https://github.com/Python-roborock/python-roborock/pull/248), [`190f66e`](https://github.com/Python-roborock/python-roborock/commit/190f66e53fca6938b927fd587ebcdb249c908505)) - Bump semantic release ([#236](https://github.com/Python-roborock/python-roborock/pull/236), [`cf067d4`](https://github.com/Python-roborock/python-roborock/commit/cf067d4e4fa4680e766719dc22295afb2a526323)) * fix: bump semantic release * fix: bump versioning and add environment * fix: move if check * fix: some other version bumps - Change to deploy_key ([#254](https://github.com/Python-roborock/python-roborock/pull/254), [`de0a0c7`](https://github.com/Python-roborock/python-roborock/commit/de0a0c73f1f9b415f67412170a754d6685f0c969)) - Change to persist credentials ([#246](https://github.com/Python-roborock/python-roborock/pull/246), [`5b4b769`](https://github.com/Python-roborock/python-roborock/commit/5b4b7694743d96ca7acb57ed28271220791f9802)) - Container issue from api change and ci update ([#257](https://github.com/Python-roborock/python-roborock/pull/257), [`b1e645d`](https://github.com/Python-roborock/python-roborock/commit/b1e645d6acb8de776f5361e2a5a2be59c730237b)) - Give ci more permissions ([#240](https://github.com/Python-roborock/python-roborock/pull/240), [`641a40c`](https://github.com/Python-roborock/python-roborock/commit/641a40c12f38f3dcdca36aa61f17663440f0ba8e)) - Hopefully finalize semantic release ([#244](https://github.com/Python-roborock/python-roborock/pull/244), [`481f01d`](https://github.com/Python-roborock/python-roborock/commit/481f01dc039f27037e269a7234c97006dae91969)) - Move github token to env for semantic release ([#241](https://github.com/Python-roborock/python-roborock/pull/241), [`c61d8de`](https://github.com/Python-roborock/python-roborock/commit/c61d8de1bbf0705d0d7a2699822e6bfef49c3db4)) - Repair semantic release ([#251](https://github.com/Python-roborock/python-roborock/pull/251), [`431bc20`](https://github.com/Python-roborock/python-roborock/commit/431bc2033340267340f4740cef14ec0e4c5e7331)) - Semantic release versioning tag ([#237](https://github.com/Python-roborock/python-roborock/pull/237), [`fcc58ee`](https://github.com/Python-roborock/python-roborock/commit/fcc58ee6de75a61642e73c63cf614d8953318c29)) - Semantic release versioning tag ([#238](https://github.com/Python-roborock/python-roborock/pull/238), [`33a1e72`](https://github.com/Python-roborock/python-roborock/commit/33a1e72d97881aac867119eddca39c4366a549e3)) * fix: semantic release versioning tag * fix: set version back - Set python version in ci ([#239](https://github.com/Python-roborock/python-roborock/pull/239), [`dcad510`](https://github.com/Python-roborock/python-roborock/commit/dcad510ec232380f5bed7646c4455f656b7ca6ae)) - Specify x-access-token ([#249](https://github.com/Python-roborock/python-roborock/pull/249), [`e9f319b`](https://github.com/Python-roborock/python-roborock/commit/e9f319b0ee22cd90e9437d20f279a24228ee62c1)) - Update_gh_token ([#242](https://github.com/Python-roborock/python-roborock/pull/242), [`8a9866c`](https://github.com/Python-roborock/python-roborock/commit/8a9866cce2f6d868ab5f87b13a6b0151034d7a22)) - Update_gh_token ([#243](https://github.com/Python-roborock/python-roborock/pull/243), [`e100ab3`](https://github.com/Python-roborock/python-roborock/commit/e100ab3e8557ed97a5917cadb40968bbf7686b76)) ### Chores - Update README.md ([`5a982b7`](https://github.com/Python-roborock/python-roborock/commit/5a982b723528e67c6d8d664dd8b3eee64436a0c8)) ## v2.8.0 (2024-11-12) ### Chores - Call to super in docs ([#235](https://github.com/Python-roborock/python-roborock/pull/235), [`df331ea`](https://github.com/Python-roborock/python-roborock/commit/df331ea0165d05b093f170fb9107918aaaac03e6)) ### Features - Add some new roborock codes and add custom command ([#234](https://github.com/Python-roborock/python-roborock/pull/234), [`c8507ef`](https://github.com/Python-roborock/python-roborock/commit/c8507eff9cdc24654034fbe4fd63ac89b6de6f99)) * fix: add some new roborock codes and add custom command * fix: lint ## v2.7.2 (2024-11-08) ### Bug Fixes - Add some new roborock codes ([#233](https://github.com/Python-roborock/python-roborock/pull/233), [`59546dd`](https://github.com/Python-roborock/python-roborock/commit/59546dd68f7b40ad368d58fd502680ff9c03c81b)) ## v2.7.1 (2024-10-28) ### Bug Fixes - Check that clean area is not a str ([#230](https://github.com/Python-roborock/python-roborock/pull/230), [`e66a91e`](https://github.com/Python-roborock/python-roborock/commit/e66a91edaf6fedf5d4b2ab9117b7759295add492)) ### Chores - Add some async improvements ([#229](https://github.com/Python-roborock/python-roborock/pull/229), [`e987c17`](https://github.com/Python-roborock/python-roborock/commit/e987c17ee65982c7179f4d94a84e1863aa4830da)) * chore: add some async improvements * chore: improve get_rand_int ## v2.7.0 (2024-10-28) ### Features - Remove dacite ([#227](https://github.com/Python-roborock/python-roborock/pull/227), [`86878a7`](https://github.com/Python-roborock/python-roborock/commit/86878a71d82c2cc707daa16dec109fc07360e3f6)) ## v2.6.1 (2024-10-22) ### Bug Fixes - Add a warning for wrong type of clean area and add new dock ([#224](https://github.com/Python-roborock/python-roborock/pull/224), [`c334eb2`](https://github.com/Python-roborock/python-roborock/commit/c334eb2193091dccd23db0d3ee4863e838733e30)) ## v2.6.0 (2024-06-29) ### Features - Add q revo pro/p10 pro support ([#220](https://github.com/Python-roborock/python-roborock/pull/220), [`5e6a2d6`](https://github.com/Python-roborock/python-roborock/commit/5e6a2d6a7171da146efb3e59ddb3215c2a573507)) ## v2.5.0 (2024-06-25) ### Features - Add some typing ([#219](https://github.com/Python-roborock/python-roborock/pull/219), [`35d0900`](https://github.com/Python-roborock/python-roborock/commit/35d09000b8d144cbaf935069952ea135950d0e78)) ## v2.4.0 (2024-06-25) ### Features - Add some missing codes and make warnings only message once ([#218](https://github.com/Python-roborock/python-roborock/pull/218), [`12361b5`](https://github.com/Python-roborock/python-roborock/commit/12361b58e7a4d368281c4ffd9ac3d8e9d8155e62)) ## v2.3.0 (2024-06-07) ### Features - Add warning in web requests if it fails to decode ([#215](https://github.com/Python-roborock/python-roborock/pull/215), [`6ae69e9`](https://github.com/Python-roborock/python-roborock/commit/6ae69e9bcba6a98736f2f480114922186f6ca458)) ## v2.2.3 (2024-06-04) ### Bug Fixes - S8 maxv has a wash and fill dock ([#213](https://github.com/Python-roborock/python-roborock/pull/213), [`018fd05`](https://github.com/Python-roborock/python-roborock/commit/018fd052360dffd238919e336943809720457c4e)) ### Chores - Add load multi map parameter to docs(#209) ([`2cee5d7`](https://github.com/Python-roborock/python-roborock/commit/2cee5d7e065473232caacf1531c38e83506f0c5b)) - Update documentation for reset_consumable ([#207](https://github.com/Python-roborock/python-roborock/pull/207), [`4071538`](https://github.com/Python-roborock/python-roborock/commit/40715387f5eac6788d198ffefad0c1d25b7c7138)) Document parameter for API function reset_consumable ## v2.2.2 (2024-05-16) ### Bug Fixes - Handle weird clean record response ([#206](https://github.com/Python-roborock/python-roborock/pull/206), [`07ce71a`](https://github.com/Python-roborock/python-roborock/commit/07ce71a2cd8085136952bd7639f6f4a2e273faf9)) ## v2.2.1 (2024-05-11) ### Bug Fixes - Add missing value "high = 203" to RoborockMopIntensityS8MaxVUltra ([#205](https://github.com/Python-roborock/python-roborock/pull/205), [`886b0e6`](https://github.com/Python-roborock/python-roborock/commit/886b0e6a8a4b98ff74964d59f4c8c0fbbf569688)) ## v2.2.0 (2024-05-09) ### Features - Improve some typing ([#204](https://github.com/Python-roborock/python-roborock/pull/204), [`7752db9`](https://github.com/Python-roborock/python-roborock/commit/7752db9066fa49bb93a6268a491e2a0baa608cfc)) ## v2.1.1 (2024-05-08) ### Bug Fixes - Set roommapping when it is only one room ([#203](https://github.com/Python-roborock/python-roborock/pull/203), [`26af66b`](https://github.com/Python-roborock/python-roborock/commit/26af66bd5d8dbfa4c94a9add317ccc9ca9161510)) * fix: set roommapping when it is only one room * fix: add len check ## v2.1.0 (2024-05-08) ### Features - Add s8_maxv_ultra info ([#202](https://github.com/Python-roborock/python-roborock/pull/202), [`aaaf0f0`](https://github.com/Python-roborock/python-roborock/commit/aaaf0f0c381924524a079f600de14db1cd61ed45)) ## v2.0.0 (2024-04-11) ### Features - Add zeo support and fix some a01 weirdness ([#200](https://github.com/Python-roborock/python-roborock/pull/200), [`e825ff5`](https://github.com/Python-roborock/python-roborock/commit/e825ff5811516b4034e9b41769e5912c99cf0166)) * major: add A01 * chore: add init * chore: fix commitlint? * chore: fix commitlint * chore: change refactor to be major tag * refactor: add A01 * feat: add a01 BREAKING CHANGE: You must now specify what version api you want to use with clients. * feat: add initial zeo support * fix: fix A01 support * fix: allow messages to fail * fix: lint * feat: add more zeo things ### Breaking Changes - You must now specify what version api you want to use with clients. ## v1.0.0 (2024-04-09) ### Chores - Move more things around in version 1 api ([#198](https://github.com/Python-roborock/python-roborock/pull/198), [`30d2577`](https://github.com/Python-roborock/python-roborock/commit/30d257756f35b9fc71d64d0479b872661b9176a6)) * chore: move more things around in version 1 api * fix: tests ### Refactoring - Add A01 ([#199](https://github.com/Python-roborock/python-roborock/pull/199), [`16b9e3e`](https://github.com/Python-roborock/python-roborock/commit/16b9e3e8261db3ec38d6bc24661ecf40c6bb0870)) * major: add A01 * chore: add init * chore: fix commitlint? * chore: fix commitlint * chore: change refactor to be major tag * refactor: add A01 * feat: add a01 BREAKING CHANGE: You must now specify what version api you want to use with clients. ### Breaking Changes - You must now specify what version api you want to use with clients. ## v0.41.0 (2024-03-06) ### Features - Add v1 api ([#194](https://github.com/Python-roborock/python-roborock/pull/194), [`9fb124e`](https://github.com/Python-roborock/python-roborock/commit/9fb124ecdd0a979ff8f2c742eb4dd625b7e9292f)) * feat: add v1 api * fix: change some imports * fix: bug and versioning * chore: move location of v1 * fix: random exception ## v0.40.0 (2024-03-03) ### Features - Add nonce to diagnostic data ([#195](https://github.com/Python-roborock/python-roborock/pull/195), [`ceafcb6`](https://github.com/Python-roborock/python-roborock/commit/ceafcb6e30c60f6f6ad3833ab73861c18413b806)) ## v0.39.2 (2024-02-26) ### Bug Fixes - Bump construct and add wm category ([#192](https://github.com/Python-roborock/python-roborock/pull/192), [`2f18b35`](https://github.com/Python-roborock/python-roborock/commit/2f18b35755776844e266c893b126a830622afd43)) ## v0.39.1 (2024-01-24) ### Bug Fixes - Remove problematic code ([#189](https://github.com/Python-roborock/python-roborock/pull/189), [`a9e12ca`](https://github.com/Python-roborock/python-roborock/commit/a9e12ca122b467d74e9cd29dc031802cf0f551bc)) ## v0.39.0 (2024-01-03) ### Chores - Added code from decompiled react and refactoring web api ([#176](https://github.com/Python-roborock/python-roborock/pull/176), [`dab105c`](https://github.com/Python-roborock/python-roborock/commit/dab105c58d11f7789b5f11dd962dd916d5436ced)) * chore: added code from decompiled react and refactoring web api * fix: patches * fix: patch * chore: add info from new_feature_info - Update api_commands.rst app_goto_target ([#163](https://github.com/Python-roborock/python-roborock/pull/163), [`9c83c77`](https://github.com/Python-roborock/python-roborock/commit/9c83c77c732943b2cb9481442afddc3b1ba241c3)) ### Features - Add async_release ([#179](https://github.com/Python-roborock/python-roborock/pull/179), [`ae58627`](https://github.com/Python-roborock/python-roborock/commit/ae58627bda324c29090b7c4ab78776288a30a64d)) ## v0.38.0 (2023-12-11) ### Features - Add information from product api ([#158](https://github.com/Python-roborock/python-roborock/pull/158), [`22720ae`](https://github.com/Python-roborock/python-roborock/commit/22720aee79e582328ae642e61d57dc2e3a92ec1c)) * fix: add information from product api * feat: add dyad protocol ## v0.37.0 (2023-12-10) ### Features - House keeping, version bumping, doc fixes, doc improvements, v2 home data api ([#157](https://github.com/Python-roborock/python-roborock/pull/157), [`f3ca9b4`](https://github.com/Python-roborock/python-roborock/commit/f3ca9b45d3de3a15c57e134421d3abc11095bc22)) * feat: version bumping, docs improvements, mypy fixes, doc fixes * fix: ci steps * feat: convert to v2 of the api * chore: linting, include docs, poetry lock * fix: tests * fix: add ability to remove listener ## v0.36.2 (2023-11-22) ### Bug Fixes - Typing and error checking ([#149](https://github.com/Python-roborock/python-roborock/pull/149), [`d94aa48`](https://github.com/Python-roborock/python-roborock/commit/d94aa48c1e594f7f6cd1cff16da66169368fb86c)) * fix: typing and error checking * chore: lint * fix: merge weirdness ## v0.36.1 (2023-11-08) ### Bug Fixes - Typing for map ([#141](https://github.com/Python-roborock/python-roborock/pull/141), [`64121ee`](https://github.com/Python-roborock/python-roborock/commit/64121eee14e4f0ca24db664b0664aaac5c7332af)) ## v0.36.0 (2023-11-07) ### Features - Update listeners ([#140](https://github.com/Python-roborock/python-roborock/pull/140), [`5498596`](https://github.com/Python-roborock/python-roborock/commit/549859669941e71c8d7ee09a0d4eea9564b4a12f)) * fix: change some typing * fix: include poetry lock * fix: linting * fix: add typing * fix: bugs * fix: none typing * fix: weird merge things * fix: rework listeners and cache a bit more * chore: linting * chore: typo * chore: self listener model * fix: override missing for data protocol ## v0.35.4 (2023-11-03) ### Bug Fixes - Mypy complaints ([#137](https://github.com/Python-roborock/python-roborock/pull/137), [`752e320`](https://github.com/Python-roborock/python-roborock/commit/752e320644449a83a724590628c4011b9d8bacb2)) * fix: change some typing * fix: include poetry lock * fix: linting * fix: add typing * fix: bugs * fix: none typing * Update api.py ## v0.35.3 (2023-10-29) ### Bug Fixes - Typing and versioning ([#134](https://github.com/Python-roborock/python-roborock/pull/134), [`e1dc545`](https://github.com/Python-roborock/python-roborock/commit/e1dc545f20f2a163240eb72d831025cb2ff3ec7c)) * fix: change some typing * fix: include poetry lock * fix: linting ### Chores - **deps**: Bump snok/install-poetry from 1.3.3 to 1.3.4 ([#106](https://github.com/Python-roborock/python-roborock/pull/106), [`1fc0265`](https://github.com/Python-roborock/python-roborock/commit/1fc02658e9d5934c5b5a2e173d7bcba8d8c55c2f)) Bumps [snok/install-poetry](https://github.com/snok/install-poetry) from 1.3.3 to 1.3.4. - [Release notes](https://github.com/snok/install-poetry/releases) - [Commits](https://github.com/snok/install-poetry/compare/v1.3.3...v1.3.4) --- updated-dependencies: - dependency-name: snok/install-poetry dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ## v0.35.2 (2023-10-29) ### Bug Fixes - Error catch and typing ([#133](https://github.com/Python-roborock/python-roborock/pull/133), [`171c302`](https://github.com/Python-roborock/python-roborock/commit/171c30265664b0161db75695d2d30d8b45bbf5b3)) ### Chores - Add some initial documentation ([#94](https://github.com/Python-roborock/python-roborock/pull/94), [`316fc0d`](https://github.com/Python-roborock/python-roborock/commit/316fc0d95f83948da25df0515622913173117ee0)) ## v0.35.1 (2023-10-28) ### Bug Fixes - Add s5 max mop code 207 ([#132](https://github.com/Python-roborock/python-roborock/pull/132), [`adc7ae0`](https://github.com/Python-roborock/python-roborock/commit/adc7ae0bbb75eb5be452efb62ca93de6a5211eef)) ## v0.35.0 (2023-10-18) ### Features - **code_mappings**: Add error n53 cleaning tank full or blocked ([#130](https://github.com/Python-roborock/python-roborock/pull/130), [`ebd57a0`](https://github.com/Python-roborock/python-roborock/commit/ebd57a0b559c0dee605e30eaead58b8433347a84)) Co-authored-by: jalcaras ## v0.34.6 (2023-10-02) ### Bug Fixes - Add missing 207 code ([#127](https://github.com/Python-roborock/python-roborock/pull/127), [`87431a1`](https://github.com/Python-roborock/python-roborock/commit/87431a1f155059a51b1b3e2c8867fe18cc476e16)) ## v0.34.5 (2023-09-29) ### Bug Fixes - Remove alexapy ([#126](https://github.com/Python-roborock/python-roborock/pull/126), [`38ff4eb`](https://github.com/Python-roborock/python-roborock/commit/38ff4eb90a1805ad599f61322d7c3547f465868b)) ## v0.34.4 (2023-09-28) ### Bug Fixes - Parsing potential list of clean record ([#125](https://github.com/Python-roborock/python-roborock/pull/125), [`df7a920`](https://github.com/Python-roborock/python-roborock/commit/df7a920a94a632d9653637e0111b3a955db49356)) ## v0.34.3 (2023-09-24) ### Bug Fixes - Add custom code for p10 ([#123](https://github.com/Python-roborock/python-roborock/pull/123), [`8b57d50`](https://github.com/Python-roborock/python-roborock/commit/8b57d50b0c898ca7d3df7cbdfe3682fd03cf649e)) ## v0.34.2 (2023-09-21) ### Bug Fixes - Make cache not global ([#122](https://github.com/Python-roborock/python-roborock/pull/122), [`e119201`](https://github.com/Python-roborock/python-roborock/commit/e119201f1c700d98e3322653440097c91ef4e14c)) * feat: add datetime parsing in cleanrecord * chore: lint * fix: timezone for non-3.11 * feat: add is_available for ha and here in future * fix: add timeout as a variable and set a longer default timeout for cloud * fix: is_available true by default * fix: status type as class variable * fix: don't update status when it was none before listener * fix: reduce info logs * fix: don't cache device cache * fix: double keepalive * fix: don't continue calling unsupported functions * fix: revert keepalive for now ## v0.34.1 (2023-09-19) ### Bug Fixes - Status reworking ([#121](https://github.com/Python-roborock/python-roborock/pull/121), [`8f4b7d3`](https://github.com/Python-roborock/python-roborock/commit/8f4b7d376d5a475798782496ea52ac9674cb9ae7)) * fix: is_available true by default * fix: status type as class variable * fix: don't update status when it was none before listener * fix: reduce info logs ## v0.34.0 (2023-09-12) ### Chores - Add pyupgrade to ruff ([#118](https://github.com/Python-roborock/python-roborock/pull/118), [`360b240`](https://github.com/Python-roborock/python-roborock/commit/360b240ab89862f8003ece11833e50846b279259)) * chore: add pyupgrade to ruff * chore: make ruff and isort play nice ### Features - Add datetime parsing in cleanrecord ([#119](https://github.com/Python-roborock/python-roborock/pull/119), [`5e67fa6`](https://github.com/Python-roborock/python-roborock/commit/5e67fa648478e573239c2f1dfc4b58c01cae1797)) * feat: add datetime parsing in cleanrecord * fix: timezone for non-3.11 * feat: add is_available for ha and here in future * fix: add timeout as a variable and set a longer default timeout for cloud ## v0.33.2 (2023-09-06) ### Bug Fixes - Add missing s5 codes ([#116](https://github.com/Python-roborock/python-roborock/pull/116), [`4d56021`](https://github.com/Python-roborock/python-roborock/commit/4d560216354fab4ab8b1d452dd6b29008b20d50a)) * fix: add missing codes for s5 max * chore: lint ## v0.33.1 (2023-09-06) ### Bug Fixes - Unknow values on HA component ([#117](https://github.com/Python-roborock/python-roborock/pull/117), [`1323618`](https://github.com/Python-roborock/python-roborock/commit/1323618c6c58bb6dcef5c7f5f2ca12e32969ba0f)) * feat add Q REVO support (RoborockFanSpeedP10 + RoborockMopModeP10) * feat add Q REVO support (model ROBOROCK_P10/roborock.vacuum.a75) * feat add Q REVO support (P10Status) * feat add Q REVO support (status data) * fix(P10Status): Change RoborockMopModeP10 by RoborockMopModeS8ProUltra * fix(RoborockMopModeP10): Remove * fix: change ordering of imports * fix: change q_revo->p10 to be consistent with entire code * fix: for HA component(items: dock_mop_wash_mode_interval, dock_washing_mode) stuck at "unknow" value when using P10 --------- Co-authored-by: jalcaras Co-authored-by: jalcaras Co-authored-by: Luke ## v0.33.0 (2023-09-04) ### Features - Add q revo/p10 support ([#114](https://github.com/Python-roborock/python-roborock/pull/114), [`b2237d9`](https://github.com/Python-roborock/python-roborock/commit/b2237d97384d819cbcc62902bbcbb2c7dbe0072e)) * feat add Q REVO support (RoborockFanSpeedP10 + RoborockMopModeP10) * feat add Q REVO support (model ROBOROCK_P10/roborock.vacuum.a75) * feat add Q REVO support (P10Status) * feat add Q REVO support (status data) * fix(P10Status): Change RoborockMopModeP10 by RoborockMopModeS8ProUltra * fix(RoborockMopModeP10): Remove * fix: change ordering of imports --------- Co-authored-by: jalcaras Co-authored-by: jalcaras Co-authored-by: Luke ## v0.32.4 (2023-08-30) ### Bug Fixes - Refactor cache and call get_status after changing mop mode ([#105](https://github.com/Python-roborock/python-roborock/pull/105), [`8bf70f4`](https://github.com/Python-roborock/python-roborock/commit/8bf70f4f8b3cabe846bffdc3dd3300f9f621ae97)) ### Chores - **deps**: Bump wagoid/commitlint-github-action from 5.4.1 to 5.4.3 ([#96](https://github.com/Python-roborock/python-roborock/pull/96), [`2da7b38`](https://github.com/Python-roborock/python-roborock/commit/2da7b3865bb1693b7ce655bf0d44090753aa5a52)) ## v0.32.3 (2023-08-05) ### Bug Fixes - Resolve unawaited task errors on connect/disconnect ([#103](https://github.com/Python-roborock/python-roborock/pull/103), [`1ad03be`](https://github.com/Python-roborock/python-roborock/commit/1ad03befa84f9b729a0cc7553b794fe5344a22ce)) * fix: resolve unawaited task errors on connect/disconnect * chore: make lint happy ## v0.32.2 (2023-08-04) ### Bug Fixes - Waiting queue ([`ff5376b`](https://github.com/Python-roborock/python-roborock/commit/ff5376be3a4ff4eb90e33118db89214ef699dc6f)) ## v0.32.1 (2023-08-04) ### Bug Fixes - Remove coroutine warning ([`da83078`](https://github.com/Python-roborock/python-roborock/commit/da83078f7ef8f333fa46b75603ce8a88bb97914d)) ## v0.32.0 (2023-08-03) ### Chores - Lint ([`d158dcc`](https://github.com/Python-roborock/python-roborock/commit/d158dcc2c44d2d529e762d95815dc854b5ed674e)) ### Features - Adding device_id to listeners and fixing race condition on connection, disconnection and messages ([`2bee8a1`](https://github.com/Python-roborock/python-roborock/commit/2bee8a11ad30cd4a3c186a4c0a619838adc83a53)) ## v0.31.1 (2023-08-02) ### Bug Fixes - Add error code for invalid credentials ([#101](https://github.com/Python-roborock/python-roborock/pull/101), [`703f48b`](https://github.com/Python-roborock/python-roborock/commit/703f48b66cfd32d20e74eaa959a66cd736ca38c8)) ## v0.31.0 (2023-07-31) ### Features - Add device name to logs ([#100](https://github.com/Python-roborock/python-roborock/pull/100), [`7690d56`](https://github.com/Python-roborock/python-roborock/commit/7690d5644181abb5fb7681d6c1764e2f8750c4b5)) ## v0.30.3 (2023-07-31) ### Bug Fixes - Adding no dustbin to docker errors ([`0e28628`](https://github.com/Python-roborock/python-roborock/commit/0e286280edda21a3b95c656d5bc358cd4229d075)) ## v0.30.2 (2023-07-21) ### Bug Fixes - Possible solution for future invalid state ([`8ac4e72`](https://github.com/Python-roborock/python-roborock/commit/8ac4e72372f26105423213bb85d4c33d7951af4d)) ## v0.30.1 (2023-07-18) ### Bug Fixes - Add missing s8 pro mop code and q revo dock ([#92](https://github.com/Python-roborock/python-roborock/pull/92), [`5d75c3b`](https://github.com/Python-roborock/python-roborock/commit/5d75c3b794db231e07f8b6693f2a96b132f737ce)) ### Chores - **deps**: Bump relekang/python-semantic-release from 7.34.6 to 8.0.0 ([#89](https://github.com/Python-roborock/python-roborock/pull/89), [`9677018`](https://github.com/Python-roborock/python-roborock/commit/96770184e953598e6232dbed4e6d39466f7d7465)) ## v0.30.0 (2023-07-10) ### Bug Fixes - Add missing dock for s7 max ultra ([#88](https://github.com/Python-roborock/python-roborock/pull/88), [`10aff22`](https://github.com/Python-roborock/python-roborock/commit/10aff22bc1e6d17b1b6c2587ebefcfd1d9fb7be7)) - Listeners getting protocol data before it exists. ([#87](https://github.com/Python-roborock/python-roborock/pull/87), [`3d68ea4`](https://github.com/Python-roborock/python-roborock/commit/3d68ea4326da827f17a32b2b5645f1e1e43f3eca)) * fix: listeners getting protocol data before it exists * fix: optimize code ### Features - Created strong foundation for docs ([#86](https://github.com/Python-roborock/python-roborock/pull/86), [`ef88edd`](https://github.com/Python-roborock/python-roborock/commit/ef88eddb8b582f5ad958d8135964e39ba6a05c91)) ## v0.29.2 (2023-06-28) ### Bug Fixes - Downgrade construct ([#84](https://github.com/Python-roborock/python-roborock/pull/84), [`920f59f`](https://github.com/Python-roborock/python-roborock/commit/920f59f1fad2790084ee001225bbaff2e21b3f91)) ## v0.29.1 (2023-06-27) ### Bug Fixes - Adding scene commands ([`fddbe50`](https://github.com/Python-roborock/python-roborock/commit/fddbe508f177dc6bc336223007018f501709c995)) ## v0.29.0 (2023-06-26) ### Features - Adding server timer and retry command compatibility ([`1a1565b`](https://github.com/Python-roborock/python-roborock/commit/1a1565b1f2eb57fa373c9298dd2501a13914bb0a)) ## v0.28.0 (2023-06-26) ### Features - Adding status and consumable listeners ([#83](https://github.com/Python-roborock/python-roborock/pull/83), [`ebdbc90`](https://github.com/Python-roborock/python-roborock/commit/ebdbc907f1f1a2a91ad10953ca6e70b91b9664dd)) * feat: adding status and consumable listeners * fix: api tests * chore: linting ## v0.27.2 (2023-06-22) ### Bug Fixes - Cache concurrency ([`7dd3aa4`](https://github.com/Python-roborock/python-roborock/commit/7dd3aa4933248ede6230a82e6d14e30e8009e27c)) ## v0.27.1 (2023-06-22) ### Bug Fixes - Improving cache and refactoring ([`e88854d`](https://github.com/Python-roborock/python-roborock/commit/e88854d3c6c9109e9fbb4e8ecd3d0ee4ad5d53ff)) ## v0.27.0 (2023-06-22) ### Features - Improving cache and refactoring ([#82](https://github.com/Python-roborock/python-roborock/pull/82), [`e6d48af`](https://github.com/Python-roborock/python-roborock/commit/e6d48af4e1c83fe79104d368918613ac0b332cbb)) ## v0.26.2 (2023-06-21) ### Bug Fixes - #81 - cli raising exception for diagnostic data ([`690b316`](https://github.com/Python-roborock/python-roborock/commit/690b316de35c970454a45418682c82d752b81201)) ## v0.26.1 (2023-06-20) ### Bug Fixes - Changelog ([#80](https://github.com/Python-roborock/python-roborock/pull/80), [`5c4928b`](https://github.com/Python-roborock/python-roborock/commit/5c4928b2d414b9decc1a454348e38d29aeb505fa)) ## v0.26.0 (2023-06-20) ### Chores - Update pyproject ([#79](https://github.com/Python-roborock/python-roborock/pull/79), [`cad97da`](https://github.com/Python-roborock/python-roborock/commit/cad97da7924288524993b32f2d2cd7d71abccee6)) - **deps**: Bump relekang/python-semantic-release from 7.34.4 to 7.34.6 ([#78](https://github.com/Python-roborock/python-roborock/pull/78), [`cebc9d2`](https://github.com/Python-roborock/python-roborock/commit/cebc9d28aa5222e78670bab5e19e162774a9a73f)) ### Features - Adding command cache ([#77](https://github.com/Python-roborock/python-roborock/pull/77), [`505f5e4`](https://github.com/Python-roborock/python-roborock/commit/505f5e45a56e98c248a38236ae3f02908583de12)) * feat: adding command cache * chore: typo * fix: dependencies * feat: adding cache evict time ## v0.25.2 (2023-06-17) ### Bug Fixes - Downgrading construct version ([`d5148ce`](https://github.com/Python-roborock/python-roborock/commit/d5148ce8fc553f73819a9f03c7688d53100bdcd9)) - Moving back to python 3.10 due to python-semantic-release incompatibility ([`8ab9352`](https://github.com/Python-roborock/python-roborock/commit/8ab9352adb2cb82c24057bef3107b28d3a157087)) - Removing python 10 tests ([`46e258b`](https://github.com/Python-roborock/python-roborock/commit/46e258bc495123c8e8325a731e353f3bc5ce3e0c)) ## v0.25.1 (2023-06-16) ### Bug Fixes - Python-semantic-release python version ([`845da45`](https://github.com/Python-roborock/python-roborock/commit/845da456a0d59765d08962fee007b63c8d0c50eb)) ## v0.25.0 (2023-06-16) ### Bug Fixes - Remove dnd timer and valley electricity from props ([#75](https://github.com/Python-roborock/python-roborock/pull/75), [`2035af5`](https://github.com/Python-roborock/python-roborock/commit/2035af5d524605fcbd0b87e20f256c1c61ca9c68)) * fix: remove dnd timer and valley electricity from props * fix: linting * fix: clear out old keep alive before adding new one * chore: remove keep_alive_task * fix: add storing of dnd and valley in api - Remove python 10 from tests ([`31fc34c`](https://github.com/Python-roborock/python-roborock/commit/31fc34c22ad9e5f06b588e6b283412902bd2959d)) - Semantic release ([#76](https://github.com/Python-roborock/python-roborock/pull/76), [`224a566`](https://github.com/Python-roborock/python-roborock/commit/224a5662d2dbdf47d5141554733a9b4aeaf8d4f2)) * fix: remove dnd timer and valley electricity from props * fix: linting * fix: clear out old keep alive before adding new one * chore: remove keep_alive_task * fix: add storing of dnd and valley in api * 0.24.2 Automatically generated by python-semantic-release * fix: add dirty tank latch error ### Chores - Add dependabot ([#70](https://github.com/Python-roborock/python-roborock/pull/70), [`cff6871`](https://github.com/Python-roborock/python-roborock/commit/cff6871012370bc8c1aaeefbea32f08c3a8d21f6)) * add dependabot * chore: update dependabot ignore - Manually releasing 0.24.1 ([`0ab69b3`](https://github.com/Python-roborock/python-roborock/commit/0ab69b3cdfb1697fdd7edb9a644f296f1dfa10a2)) - Updating ci.yml ([`d4c2714`](https://github.com/Python-roborock/python-roborock/commit/d4c2714a5800c38333d292f1bef0c17a38326e40)) - **deps**: Bump wagoid/commitlint-github-action from 5.3.0 to 5.4.1 ([#71](https://github.com/Python-roborock/python-roborock/pull/71), [`951dd5c`](https://github.com/Python-roborock/python-roborock/commit/951dd5c13030e0bc15256d414ed8e11235ff192b)) Bumps [wagoid/commitlint-github-action](https://github.com/wagoid/commitlint-github-action) from 5.3.0 to 5.4.1. - [Changelog](https://github.com/wagoid/commitlint-github-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/wagoid/commitlint-github-action/compare/v5.3.0...v5.4.1) --- updated-dependencies: - dependency-name: wagoid/commitlint-github-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Update pycryptodome requirement ([#73](https://github.com/Python-roborock/python-roborock/pull/73), [`52dd451`](https://github.com/Python-roborock/python-roborock/commit/52dd451b57e7d292c6f8f01f1777f7a5cb88918b)) Updates the requirements on [pycryptodome](https://github.com/Legrandin/pycryptodome) to permit the latest version. - [Release notes](https://github.com/Legrandin/pycryptodome/releases) - [Changelog](https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst) - [Commits](https://github.com/Legrandin/pycryptodome/compare/v3.17.0...v3.18.0) --- updated-dependencies: - dependency-name: pycryptodome dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ### Features - Bump python version ([`aae48b1`](https://github.com/Python-roborock/python-roborock/commit/aae48b1395698136ca90b7fe7386a1b6ea8aaa9c)) ## v0.24.1 (2023-06-14) ### Bug Fixes - Device_prop update ([`b6d1ccc`](https://github.com/Python-roborock/python-roborock/commit/b6d1ccc913cff1a7e25745867435146e9f748df7)) - Python-semantic-release ([`80e9c24`](https://github.com/Python-roborock/python-roborock/commit/80e9c24a39f3147b0fbc0a5437631777ab52b027)) ### Chores - Manually releasing 0.24.0 ([`0a08c97`](https://github.com/Python-roborock/python-roborock/commit/0a08c972dae32a8d5670fd049b8220a4af1d3307)) ## v0.24.0 (2023-06-14) ### Features - Adding valley_electricity_timer to props ([`0844067`](https://github.com/Python-roborock/python-roborock/commit/08440670a7fb098f5f3954e2ad09f9a32e64a54e)) ## v0.23.6 (2023-06-08) ### Bug Fixes - Add datetime_time back ([#68](https://github.com/Python-roborock/python-roborock/pull/68), [`a3461dd`](https://github.com/Python-roborock/python-roborock/commit/a3461dd0a08702add2625df8616ba20d239805ce)) ### Chores - Linting ([`90f905d`](https://github.com/Python-roborock/python-roborock/commit/90f905d331125c8536ab1db29444685fcf8bf196)) ## v0.23.5 (2023-06-08) ### Bug Fixes - Issue building roborock message ([`89e1f28`](https://github.com/Python-roborock/python-roborock/commit/89e1f28461baaf03029679aed5f91200bb7dac4e)) ## v0.23.4 (2023-06-06) ### Bug Fixes - Adding method parse_datetime_to_roborock_datetime ([`64c8159`](https://github.com/Python-roborock/python-roborock/commit/64c8159a9695374a4b0599a317418949bdd8f3fe)) ### Chores - Fix mypy ([`c0e7997`](https://github.com/Python-roborock/python-roborock/commit/c0e7997c61f9878436ae65aa8530b1c08b503ed9)) ## v0.23.3 (2023-06-05) ### Bug Fixes - Parse_time_to_datetime method ([`d0fc149`](https://github.com/Python-roborock/python-roborock/commit/d0fc1498e20217d28703455937f760ba45053c61)) ## v0.23.2 (2023-06-05) ### Bug Fixes - Parse_time_to_datetime method ([`bcbc211`](https://github.com/Python-roborock/python-roborock/commit/bcbc2117dd306c21495c1f3364aa3205b3c5cfce)) ## v0.23.1 (2023-06-05) ### Bug Fixes - Parse_time_to_datetime method ([`1c39216`](https://github.com/Python-roborock/python-roborock/commit/1c39216c0ee6a29d350d08adc5d662d8669f85cf)) ## v0.23.0 (2023-06-05) ### Bug Fixes - Merging timer entities ([`22ff7f4`](https://github.com/Python-roborock/python-roborock/commit/22ff7f451166bcfda360552e92d661d0520886ae)) ### Chores - Linting ([`9e2a3c5`](https://github.com/Python-roborock/python-roborock/commit/9e2a3c5f2908c3e69e14bda239112cc6d8bbca15)) ### Features - Add diagnostic data and extra containers ([#67](https://github.com/Python-roborock/python-roborock/pull/67), [`59ef6f4`](https://github.com/Python-roborock/python-roborock/commit/59ef6f4d5366859ba5d02ba66ec1aa2288564179)) * feat: add diagnostic data and extra containers * fix: lint * fix: dock summary as roborockbase * fix: make deviceprop RoborockBase * merge in changes ## v0.22.0 (2023-06-05) ### Features - Adding type cast for send_command ([`4a0b709`](https://github.com/Python-roborock/python-roborock/commit/4a0b70997080012e3059150da2b12fb47f6ef43a)) ## v0.21.1 (2023-06-05) ### Bug Fixes - Cli json serializing ([#66](https://github.com/Python-roborock/python-roborock/pull/66), [`ab13b53`](https://github.com/Python-roborock/python-roborock/commit/ab13b53a15822067112edda285c6feddf389a8b8)) ## v0.21.0 (2023-06-04) ### Features - Add time datetime for valley ([#65](https://github.com/Python-roborock/python-roborock/pull/65), [`c965862`](https://github.com/Python-roborock/python-roborock/commit/c965862f5b8b1f4dfbc83738cdebc1e11122c387)) ## v0.20.2 (2023-06-02) ### Bug Fixes - S6maxvstatus and minor changes ([`01f84ae`](https://github.com/Python-roborock/python-roborock/commit/01f84ae741dd3c9fa3bc5932b718abebcc8e3f0f)) ## v0.20.1 (2023-06-01) ### Bug Fixes - S8 model name and adding api methods get_child_lock_status and get_sound_volume ([`a3b7cee`](https://github.com/Python-roborock/python-roborock/commit/a3b7cee63a70746ac3db5e5cee37c5b507b99478)) ## v0.20.0 (2023-05-31) ### Features - Adds code for duct blockage ([#64](https://github.com/Python-roborock/python-roborock/pull/64), [`84dd5fb`](https://github.com/Python-roborock/python-roborock/commit/84dd5fbdefebe4b33c6bae6879137847522b1bfb)) ## v0.19.0 (2023-05-31) ### Features - Moving clean area to api ([#63](https://github.com/Python-roborock/python-roborock/pull/63), [`7ade218`](https://github.com/Python-roborock/python-roborock/commit/7ade218e3efd44159c6ad40cd88933385bbd1496)) ## v0.18.10 (2023-05-30) ### Bug Fixes - Dict with enum instead of value ([`9653c50`](https://github.com/Python-roborock/python-roborock/commit/9653c50f31b03ce2d3d21e2042d5c194924f4aca)) ## v0.18.9 (2023-05-28) ### Bug Fixes - Mqtt reconnections ([`462d4e4`](https://github.com/Python-roborock/python-roborock/commit/462d4e4a30372c143c9198c7008808ca11800af5)) ### Chores - Linting ([`f850cd1`](https://github.com/Python-roborock/python-roborock/commit/f850cd1f7d10b774516e76f3dac1ba2fec254ad7)) ## v0.18.8 (2023-05-28) ### Bug Fixes - Improve device ping ([`56e4469`](https://github.com/Python-roborock/python-roborock/commit/56e4469c95ac9255604025df99f0d6ac1940dd19)) ## v0.18.7 (2023-05-27) ### Bug Fixes - Change e2 fan codes ([#62](https://github.com/Python-roborock/python-roborock/pull/62), [`7231f1e`](https://github.com/Python-roborock/python-roborock/commit/7231f1efc412f93bfb5719091337536bcb6185d6)) * fix: change e2 fan codes * fix: linting * fix: incorrect balanced code ## v0.18.6 (2023-05-19) ### Bug Fixes - Consumables with time equals 0 ([`ccab5f0`](https://github.com/Python-roborock/python-roborock/commit/ccab5f0724854ae27bbc51b9ee33f2a96ce709f1)) ## v0.18.5 (2023-05-16) ### Bug Fixes - Connection_lost ([`c2ba673`](https://github.com/Python-roborock/python-roborock/commit/c2ba673f2c198bc78e75e1cf6fc9844e385e85bb)) ## v0.18.4 (2023-05-16) ### Bug Fixes - Minor fixes ([`e4a291d`](https://github.com/Python-roborock/python-roborock/commit/e4a291dd2b011e5852c992dbb23068ef5dde0e52)) ## v0.18.3 (2023-05-15) ### Bug Fixes - Keep_alive_func ([`e4aeebc`](https://github.com/Python-roborock/python-roborock/commit/e4aeebc16317a5c9fe3ffcd3bff89be1f2070dbb)) ### Chores - Linting ([`dbffaab`](https://github.com/Python-roborock/python-roborock/commit/dbffaaba59214015a9b721347331b37ff38fb941)) ## v0.18.2 (2023-05-15) ### Bug Fixes - Adding hello command ([`dfa44ff`](https://github.com/Python-roborock/python-roborock/commit/dfa44ff56a794f30e7c93d0a9a270f2a02da7e65)) - Improving new protocols ([`08c6f95`](https://github.com/Python-roborock/python-roborock/commit/08c6f9530b202d17ef80047c2d60836f9f9b8422)) ## v0.18.1 (2023-05-15) ### Bug Fixes - Type checks ([`58b3322`](https://github.com/Python-roborock/python-roborock/commit/58b33225b50a221a5f3100055fe28461f5cff884)) ## v0.18.0 (2023-05-15) ### Features - Keep connection alive ([`691b04b`](https://github.com/Python-roborock/python-roborock/commit/691b04b0135a38cc6b150e284d96e217f18f7f46)) ## v0.17.8 (2023-05-15) ### Bug Fixes - Trying to fix connection leaks ([`a66482a`](https://github.com/Python-roborock/python-roborock/commit/a66482a22cba9a6e7cc449c3f35acc1f230cd211)) ## v0.17.7 (2023-05-15) ### Bug Fixes - Ignoring get_room_mapping for int list response ([`c71d3b5`](https://github.com/Python-roborock/python-roborock/commit/c71d3b549a8dd09d08d1d27cde6882298875269c)) ## v0.17.6 (2023-05-13) ### Bug Fixes - Using cache only a single time ([`1ebfb35`](https://github.com/Python-roborock/python-roborock/commit/1ebfb35b9fe9ec50d4abeb60c695d33a37818768)) ## v0.17.5 (2023-05-12) ### Bug Fixes - Adding log for local disconnection ([`3001798`](https://github.com/Python-roborock/python-roborock/commit/300179839ec6a25e4ab8172f2c11e8beb0ff17ce)) ## v0.17.4 (2023-05-12) ### Bug Fixes - Pycharm typing ([`12d7c0b`](https://github.com/Python-roborock/python-roborock/commit/12d7c0b71bdeae90e9abbc6a16de3e07ebaa82da)) ## v0.17.3 (2023-05-12) ### Bug Fixes - Trigger new release ([`270a65c`](https://github.com/Python-roborock/python-roborock/commit/270a65c24a847cdc58a630e6d6c8e296910de8ea)) ## v0.17.2 (2023-05-11) ### Bug Fixes - Adding fallback cache (to be tested) ([`0e214cd`](https://github.com/Python-roborock/python-roborock/commit/0e214cd0633e9b9baca3323cc505a4f787aa08fb)) - Fallback_cache func ([`8048d84`](https://github.com/Python-roborock/python-roborock/commit/8048d843f669b06960967918570201498e4ae051)) ### Chores - Linting ([`2263190`](https://github.com/Python-roborock/python-roborock/commit/226319078162796c186bcd0bef46b961153e0435)) ## v0.17.1 (2023-05-11) ### Bug Fixes - Improving logs ([`cdd0ea7`](https://github.com/Python-roborock/python-roborock/commit/cdd0ea75d4e336c8f918a79574fd7b642eaffeec)) ## v0.17.0 (2023-05-11) ### Features - Dynamic calculated prefixes ([`d57a0a7`](https://github.com/Python-roborock/python-roborock/commit/d57a0a7d31f851b6bf4381233a84187d19e5782f)) ## v0.16.1 (2023-05-10) ### Bug Fixes - Connection timeouts ([`36a7295`](https://github.com/Python-roborock/python-roborock/commit/36a7295ce878dd0649505dd4a5b5ad662f0655fd)) ## v0.16.0 (2023-05-10) ### Chores - Adding package_parser.py ([`c6cc29b`](https://github.com/Python-roborock/python-roborock/commit/c6cc29b86418c7ed62f30a5684f5a95a6a712834)) - Fix readthedocs ([#59](https://github.com/Python-roborock/python-roborock/pull/59), [`b747ad8`](https://github.com/Python-roborock/python-roborock/commit/b747ad89ec1180ceffc4130d1be1ce9dee203f98)) - Linting ([`3eaed1d`](https://github.com/Python-roborock/python-roborock/commit/3eaed1d48293f474e65914c17c93ea54b7c0a9a5)) ### Features - Adding pcap file parser to cli ([`798287a`](https://github.com/Python-roborock/python-roborock/commit/798287a5100a3e973524aae6dd9404c0af354c11)) ## v0.15.0 (2023-05-09) ### Bug Fixes - Add int for clean summary ([#57](https://github.com/Python-roborock/python-roborock/pull/57), [`4257aa7`](https://github.com/Python-roborock/python-roborock/commit/4257aa7888178703d1b38ed00c12ef932ca1e862)) ### Features - Add docs ([#58](https://github.com/Python-roborock/python-roborock/pull/58), [`959abe1`](https://github.com/Python-roborock/python-roborock/commit/959abe1f3b2be0bfb8705d1bc1f9cbe966577540)) ## v0.14.1 (2023-05-09) ### Bug Fixes - Add types for S8 ([#56](https://github.com/Python-roborock/python-roborock/pull/56), [`125b6e7`](https://github.com/Python-roborock/python-roborock/commit/125b6e728145fde39f49fa6b80168bb985f2cc43)) * fix: add types for S8 * fix: lint ## v0.14.0 (2023-05-08) ### Features - Add more codes for status ([#55](https://github.com/Python-roborock/python-roborock/pull/55), [`cddd765`](https://github.com/Python-roborock/python-roborock/commit/cddd765aa15e31ae50db5a6b29ff6988050aa5cc)) ## v0.13.4 (2023-05-05) ### Bug Fixes - Command prefixes ([`65c5db8`](https://github.com/Python-roborock/python-roborock/commit/65c5db834baadc4c1a61704bd2279c48dd0f6074)) ## v0.13.3 (2023-05-05) ### Bug Fixes - Roborock enum ([`ae0b93e`](https://github.com/Python-roborock/python-roborock/commit/ae0b93ee0f0fc9c62c3f40b436ece209938e9e6c)) ### Chores - Linting ([`250d5fc`](https://github.com/Python-roborock/python-roborock/commit/250d5fcc0a320604ee25519764bd7ac1872dbd0b)) - Linting ([`fea34d6`](https://github.com/Python-roborock/python-roborock/commit/fea34d63400a94447834ab355d0a023b53e77d7d)) ## v0.13.2 (2023-05-05) ### Bug Fixes - Minor changes ([`522734a`](https://github.com/Python-roborock/python-roborock/commit/522734a4bdcf6555feede24e3e97c6a3a98fa760)) ## v0.13.1 (2023-05-05) ### Bug Fixes - Adding app_start_collect_dust prefix ([`3124d7e`](https://github.com/Python-roborock/python-roborock/commit/3124d7ea6277ec08d8e592448b2a4f8cb60fb7db)) ## v0.13.0 (2023-05-05) ### Features - Add s4_max ([#54](https://github.com/Python-roborock/python-roborock/pull/54), [`e7cfd15`](https://github.com/Python-roborock/python-roborock/commit/e7cfd153b3c41215fd1c85d4968a14d1862c91b5)) ## v0.12.1 (2023-05-05) ### Bug Fixes - Changed incorrect s8 pro ultra string ([`c6a37a9`](https://github.com/Python-roborock/python-roborock/commit/c6a37a97da9279af3a6a24dc0fd01770cdd9b3b1)) fixes #52 ## v0.12.0 (2023-05-05) ### Features - Extending device status by device model ([#51](https://github.com/Python-roborock/python-roborock/pull/51), [`8092b67`](https://github.com/Python-roborock/python-roborock/commit/8092b67b8c9a380cca5178217fde3a61746fcf75)) * feat: extending device status by device model * chore: linting ## v0.11.0 (2023-05-04) ### Features - Add error check for invalid user agreement ([#49](https://github.com/Python-roborock/python-roborock/pull/49), [`0374449`](https://github.com/Python-roborock/python-roborock/commit/0374449d7280c93ceb772b7fbe009c6d19d0c462)) * minor: add error check for invalid user agreement * fix: lint * feat: add no user agreement error * fix: version issue * fix: added account to str ## v0.10.3 (2023-05-04) ### Bug Fixes - Port already in use ([`e5d71d8`](https://github.com/Python-roborock/python-roborock/commit/e5d71d88f5144c172482cd6ee71d9a5b01dbbe3f)) ## v0.10.2 (2023-05-03) ### Bug Fixes - Change devices fan speed enum to lower case ([`c559d40`](https://github.com/Python-roborock/python-roborock/commit/c559d40183e47ef8698651281ae8946a99cb897e)) - Test errors ([`6a46515`](https://github.com/Python-roborock/python-roborock/commit/6a465157bbf6fa15bc578a1c4b1dffa17a694a92)) ## v0.10.1 (2023-05-03) ### Bug Fixes - Allow discovering multiple devices ([`ada9e07`](https://github.com/Python-roborock/python-roborock/commit/ada9e0723728b1d7e3ccd6dc37cbbe06a3c6a2cc)) ### Chores - Using python construct for data parsing ([#48](https://github.com/Python-roborock/python-roborock/pull/48), [`71f7f22`](https://github.com/Python-roborock/python-roborock/commit/71f7f2207986cb22c2990ae6d67fd38c2d04b472)) * chore: using python construct for data parsing * chore: linting * fix: roborock message protocol * fix: change local api constructor ## v0.10.0 (2023-05-03) ### Chores - Linting ([`e3f2541`](https://github.com/Python-roborock/python-roborock/commit/e3f25419fcfe00f18e0cca9214c4d50cd5254c80)) ### Features - Add specific device functionality ([#46](https://github.com/Python-roborock/python-roborock/pull/46), [`32abce5`](https://github.com/Python-roborock/python-roborock/commit/32abce5d51d14aab9adef5b9560ceee534186b1a)) * feat: add support for old mop and vacuum codes * fix: linting * feat: using api for single device and adding new commands * fix: using single device api (cherry picked from commit e689e8d141acff998fd524ace923621fc0f91d0c) * chore: linting (cherry picked from commit 2ed367cba5e9b4199fdea935305fb47f85a8c1e7) (cherry picked from commit 58b46835d609794210f8c49daddbc7d25cee011d) * chore: init work * feat: added more device specific * fix: merge issues * feat: finalize specific device work * feat: finished specific device with current info * fix: add fast for S8 * fix: add s8 dock --------- Co-authored-by: humbertogontijo ## v0.9.0 (2023-05-01) ### Chores - Linting ([`a6a55ac`](https://github.com/Python-roborock/python-roborock/commit/a6a55ac4d11d230a0599aeec3d5254895fbaa684)) ### Features - Single device api and discovery method ([`5fef26d`](https://github.com/Python-roborock/python-roborock/commit/5fef26d257433c12d38f6b19731018e54884a150)) ## v0.8.3 (2023-04-28) ### Bug Fixes - Add functionality for missing enum values ([#43](https://github.com/Python-roborock/python-roborock/pull/43), [`49d77f8`](https://github.com/Python-roborock/python-roborock/commit/49d77f8208a65cb0fb86ab7948138df0bf447e45)) * fix: add functionality for missing enum values * fix: temp removed 207 * Revert "chore: linting" This reverts commit 58b46835d609794210f8c49daddbc7d25cee011d. This reverts commit 2ed367cba5e9b4199fdea935305fb47f85a8c1e7. * Revert "fix: using single device api" This reverts commit e689e8d141acff998fd524ace923621fc0f91d0c. ### Chores - Linting ([`58b4683`](https://github.com/Python-roborock/python-roborock/commit/58b46835d609794210f8c49daddbc7d25cee011d)) - Linting ([`2ed367c`](https://github.com/Python-roborock/python-roborock/commit/2ed367cba5e9b4199fdea935305fb47f85a8c1e7)) ## v0.8.2 (2023-04-27) ### Bug Fixes - Using single device api ([`e689e8d`](https://github.com/Python-roborock/python-roborock/commit/e689e8d141acff998fd524ace923621fc0f91d0c)) ### Chores - Linting ([`2e8e307`](https://github.com/Python-roborock/python-roborock/commit/2e8e307e6d82e045856d2a4ae731feba25005fe4)) ## v0.8.1 (2023-04-27) ### Bug Fixes - Adding keepalive to local connection ([`8ff8d2f`](https://github.com/Python-roborock/python-roborock/commit/8ff8d2f13fd85df96b3b334456799244ac878fbe)) ## v0.8.0 (2023-04-27) ### Features - Added error check and deviceprop functionality for core ([#42](https://github.com/Python-roborock/python-roborock/pull/42), [`746eec9`](https://github.com/Python-roborock/python-roborock/commit/746eec99ae0b6115fea6277f51b546036f7b3f18)) * feat: added update to deviceprop * feat: added time remaining to consumable * feat: added more exception checking * fix: linting * feat: add consumable const ## v0.7.8 (2023-04-26) ### Bug Fixes - Local api failing to send message ([`4cc38fe`](https://github.com/Python-roborock/python-roborock/commit/4cc38fe13df487296efda2a1e962c238e3d69168)) ### Chores - Linting ([`c378036`](https://github.com/Python-roborock/python-roborock/commit/c3780369a2ea237f7ed6f5114d68d55fff6b1386)) ## v0.7.7 (2023-04-26) ### Bug Fixes - Local api recover after command fail ([`cb11f14`](https://github.com/Python-roborock/python-roborock/commit/cb11f14d7b771b31c77dafe6435bcd52527c16a8)) ## v0.7.6 (2023-04-26) ### Bug Fixes - Reset_consumable command prefix ([`a1a8c06`](https://github.com/Python-roborock/python-roborock/commit/a1a8c06d369e33e4ebd42cf6f563b9727d0ce24e)) ### Chores - Linting ([`ac7e15a`](https://github.com/Python-roborock/python-roborock/commit/ac7e15a349aa7a6f438339109189d9d715dfa71d)) - Linting ([`4907044`](https://github.com/Python-roborock/python-roborock/commit/4907044e1933ab8afc30f2289df0ca1130cadb28)) ## v0.7.5 (2023-04-25) ### Bug Fixes - Adding missing prefixes ([`66b1833`](https://github.com/Python-roborock/python-roborock/commit/66b183385c96dd7ee395bff143f2d64ef8fb927a)) ### Chores - Linting ([`41af0e2`](https://github.com/Python-roborock/python-roborock/commit/41af0e2469cb2d9786ceab8fbcfdb4701714db69)) - Linting ([`6d6dff5`](https://github.com/Python-roborock/python-roborock/commit/6d6dff5a0131b9a6735023ce0ac47bc9a0622bc9)) ## v0.7.4 (2023-04-25) ### Bug Fixes - Get_room_mapping ([`459119b`](https://github.com/Python-roborock/python-roborock/commit/459119bee90513451bf10a1abeeccb75f3daa539)) ## v0.7.3 (2023-04-25) ### Bug Fixes - Added missing docks ([#40](https://github.com/Python-roborock/python-roborock/pull/40), [`65a6cc4`](https://github.com/Python-roborock/python-roborock/commit/65a6cc4fd19a30bc78f2c34b407d3d88e3aac2b1)) ## v0.7.2 (2023-04-25) ### Bug Fixes - Command prefixes ([`e792728`](https://github.com/Python-roborock/python-roborock/commit/e7927288cc3059a1eced1a65b31f84190718aaf2)) ## v0.7.1 (2023-04-25) ### Bug Fixes - Command prefixes ([`156ac51`](https://github.com/Python-roborock/python-roborock/commit/156ac5182d1a97c93ab16696099c8c099a19155d)) ## v0.7.0 (2023-04-25) ### Features - Add room mapping ([#41](https://github.com/Python-roborock/python-roborock/pull/41), [`aa3e6e4`](https://github.com/Python-roborock/python-roborock/commit/aa3e6e442fbbb679c4eca68840c4d19f9c659fde)) * feat: add room mapping * fix: lint * chore: move room mapping to super class client * chore: linting * Update roborock/api.py Co-authored-by: Humberto Gontijo --------- ## v0.6.17 (2023-04-25) ### Bug Fixes - Adding multi_maps_list to device props ([`7ac0485`](https://github.com/Python-roborock/python-roborock/commit/7ac0485c4a5bb43350c51331323c6773ff1c54fc)) - Removing non-needed classes ([`6ceedad`](https://github.com/Python-roborock/python-roborock/commit/6ceedadf09c20c743c994b07489887e344cd3061)) ## v0.6.16 (2023-04-22) ### Bug Fixes - Improving local integration ([`7657617`](https://github.com/Python-roborock/python-roborock/commit/7657617901d807908e5fd5c364700851b5108ab4)) ## v0.6.15 (2023-04-21) ### Bug Fixes - Get_clean_summary ([`ee81538`](https://github.com/Python-roborock/python-roborock/commit/ee815380a8b70efbac65627fdd69fdf0bb75420e)) ### Chores - Linting ([`0d3b000`](https://github.com/Python-roborock/python-roborock/commit/0d3b00093395a706ec202c5a55639ed9ece54281)) - Linting ([`124fa11`](https://github.com/Python-roborock/python-roborock/commit/124fa115b14430b2a9680d4b1da36f1b70ae85b5)) ## v0.6.14 (2023-04-21) ### Bug Fixes - Get_multi_map_list ([`cfaeb41`](https://github.com/Python-roborock/python-roborock/commit/cfaeb419e188510ade5bc1506214c9b3d2afeb18)) - Linting ([`fdb4484`](https://github.com/Python-roborock/python-roborock/commit/fdb44840741cd6872f7defea70e8f118a9803099)) ## v0.6.13 (2023-04-20) ### Bug Fixes - Check dock_type is not none ([#38](https://github.com/Python-roborock/python-roborock/pull/38), [`84c95e3`](https://github.com/Python-roborock/python-roborock/commit/84c95e3b3bebd940b9cc6cc06b73c1770605c765)) ## v0.6.12 (2023-04-19) ### Bug Fixes - Removed enum type check ([#37](https://github.com/Python-roborock/python-roborock/pull/37), [`585238e`](https://github.com/Python-roborock/python-roborock/commit/585238e505e685e14d867b19819815e7c3e19634)) ## v0.6.11 (2023-04-18) ### Bug Fixes - Lint ([`b0d8996`](https://github.com/Python-roborock/python-roborock/commit/b0d8996d46c2a52f87a8c01eb50fd6aa7bd98ed8)) ## v0.6.10 (2023-04-18) ### Bug Fixes - Lint ([`5ae44e2`](https://github.com/Python-roborock/python-roborock/commit/5ae44e247efca5e9b7958b887f6049f09ae2ced8)) ## v0.6.9 (2023-04-18) ### Bug Fixes - Lint ([`8499522`](https://github.com/Python-roborock/python-roborock/commit/8499522e5fb44abad20af1cfb7a677ca4e03639f)) ## v0.6.8 (2023-04-18) ### Bug Fixes - Lint ([`20bf54b`](https://github.com/Python-roborock/python-roborock/commit/20bf54b0a1834065584bdcb469a3123700c68f1d)) ## v0.6.7 (2023-04-18) ## v0.6.6 (2023-04-17) ### Bug Fixes - Using asyncio future instead of queue ([`1ea5430`](https://github.com/Python-roborock/python-roborock/commit/1ea5430197620dbd2dc87949e4326f24601f4ba8)) ## v0.6.5 (2023-04-13) ### Bug Fixes - Clean_summary for older devices ([`0a0c9e7`](https://github.com/Python-roborock/python-roborock/commit/0a0c9e7c965c183df971e11bd597319c68c8f646)) - Exclude changelog.md from pre-commit ([#36](https://github.com/Python-roborock/python-roborock/pull/36), [`b12c7a2`](https://github.com/Python-roborock/python-roborock/commit/b12c7a229dfdbe0af182d6a120548100b0ca4140)) ### Chores - Fix mypy errors ([#34](https://github.com/Python-roborock/python-roborock/pull/34), [`16bd2d1`](https://github.com/Python-roborock/python-roborock/commit/16bd2d1fab65760670252120fafa4b8e87e968be)) * chore: fix mypy errors * fix: run mypy through pre-commit * fix: spacing for ci * fix: tests changes * fix: cli exclusion * fix: add typing for roborockenum * fix: ignore warnings with mqtt.client * fix: more mypy changes * fix: limit cli mypy * fix: ignore type for containers * fix: add pre-commit information to dev poetry dependencies - New styling ([#35](https://github.com/Python-roborock/python-roborock/pull/35), [`55e6426`](https://github.com/Python-roborock/python-roborock/commit/55e6426129ec70f41a019fd9408b227fb8a03b5a)) ## v0.6.4 (2023-04-11) ### Bug Fixes - Disconnect on timeout so next command can work ([`5ad397b`](https://github.com/Python-roborock/python-roborock/commit/5ad397b3bbb4bc600888baba6c0cc15be9d17ef7)) ## v0.6.3 (2023-04-11) ### Bug Fixes - Semantic_release ([`63b249d`](https://github.com/Python-roborock/python-roborock/commit/63b249d65d3fc40b048320e6596aedc40f588bf9)) ## v0.6.2 (2023-04-11) ### Bug Fixes - Error code nogo_zone_detected ([`722e4b5`](https://github.com/Python-roborock/python-roborock/commit/722e4b5cfd0c4891adc506e9fe99740860027670)) ## v0.6.1 (2023-04-10) ### Bug Fixes - Lowercase true ([`774c3cc`](https://github.com/Python-roborock/python-roborock/commit/774c3cc9765ee76a3a553ca6911751124ae7164c)) - Semantic release not updating changelong ([`eaf6e90`](https://github.com/Python-roborock/python-roborock/commit/eaf6e90264b6ab69549da0e5bc3d17c4c0a2c07c)) - Trigger release ([`f1ce0ed`](https://github.com/Python-roborock/python-roborock/commit/f1ce0ed55a254bccd8567b48974ff74dd9ec8b25)) - Trigger release ([`9a4462c`](https://github.com/Python-roborock/python-roborock/commit/9a4462c800762393cc047085156acbe119cd0fe4)) - Trigger release ([`b7a664b`](https://github.com/Python-roborock/python-roborock/commit/b7a664b15b7c5180d816de325537693f47c24860)) - Trigger release ([`9256849`](https://github.com/Python-roborock/python-roborock/commit/9256849252f019f4fea2f59384bc0ea7c57adb5c)) ### Chores - Update gh token ([`f13690d`](https://github.com/Python-roborock/python-roborock/commit/f13690de8c4b5eb3d72809dff66a0caf275476dc)) ## v0.6.0 (2023-04-08) ### Bug Fixes - Changed prefixes for debugged commands ([`0db6b6d`](https://github.com/Python-roborock/python-roborock/commit/0db6b6dc3b7ef1b7721b8a9536affdd08380d916)) ### Features - Add more commands and prefixes ([`fe85dea`](https://github.com/Python-roborock/python-roborock/commit/fe85deaa1acc053c9c18f2b313ff5b812ba0e2c3)) ## v0.5.9 (2023-04-07) ### Bug Fixes - Assume device prop attr can be none ([`573db33`](https://github.com/Python-roborock/python-roborock/commit/573db337664be1f768254e384e3eef6c957955ba)) - Change to dataclass ([`111d762`](https://github.com/Python-roborock/python-roborock/commit/111d7627aa5999fc82cde650326857e51c4dc4a2)) ## v0.5.8 (2023-04-07) ### Bug Fixes - Changed prefix for set_custom_mode ([`d187eb4`](https://github.com/Python-roborock/python-roborock/commit/d187eb467e6c5c969fcaa48dcc7881d75784663d)) ## v0.5.7 (2023-04-07) ## v0.5.6 (2023-04-06) ### Bug Fixes - Create function for creating roborock code ([`2cf00fe`](https://github.com/Python-roborock/python-roborock/commit/2cf00fe607c7b5b544ea9671dabf87454cdb2322)) - Roborockbase.as_dict ([`bf52b44`](https://github.com/Python-roborock/python-roborock/commit/bf52b44b01e93000268c9fa274a3449ac3f82e36)) ## v0.5.5 (2023-04-06) ### Bug Fixes - Fix cloud_api ([`6159412`](https://github.com/Python-roborock/python-roborock/commit/6159412b577efa3544add18982d6a9859ad8225d)) ## v0.5.4 (2023-04-06) ### Bug Fixes - Minor fixes ([`7579ad5`](https://github.com/Python-roborock/python-roborock/commit/7579ad5266f46102b90be0a7676e5c116f5daefa)) ## v0.5.3 (2023-04-06) ### Bug Fixes - Roborock enum ([`df1262e`](https://github.com/Python-roborock/python-roborock/commit/df1262ef41b2b1cb4fd866cda1527b82723d38cd)) ## v0.5.2 (2023-04-06) ### Bug Fixes - Changing code mappings ([`493ed4b`](https://github.com/Python-roborock/python-roborock/commit/493ed4b9a1fb8f62918ecc4899b9ce716801b4be)) - Code mappings ([`115dad2`](https://github.com/Python-roborock/python-roborock/commit/115dad22c0280edf1853de43ae86ff1169707f5b)) - Roborockdeviceinfo ([`1ced9e9`](https://github.com/Python-roborock/python-roborock/commit/1ced9e95a6d2effb359008c2c5ef340db3243d6e)) - Using dataclass for containers ([`ad25a44`](https://github.com/Python-roborock/python-roborock/commit/ad25a443fb697f90b10a9c42c93bccbf4204c383)) ## v0.5.1 (2023-04-05) ## v0.5.0 (2023-04-05) ### Bug Fixes - Change device info class to dataclass ([`158766f`](https://github.com/Python-roborock/python-roborock/commit/158766fcb70b92aba87e8b7fe2255528fa72f123)) ### Features - Add networking function ([`19746aa`](https://github.com/Python-roborock/python-roborock/commit/19746aa7739da295c4e7c7316596af9f8ff6b0a0)) ## v0.4.16 (2023-04-05) ### Bug Fixes - Mapping prefix for all known commands ([`ad3afc0`](https://github.com/Python-roborock/python-roborock/commit/ad3afc04dfec31a20a4a2635b4c6b52cf236ce17)) ## v0.4.15 (2023-04-04) ### Bug Fixes - Test_get_washing_mode ([`17e72c3`](https://github.com/Python-roborock/python-roborock/commit/17e72c34c6ac133025450eab68f4be7025ab138b)) - **local_api**: Receiving multiple messages ([`e3c419c`](https://github.com/Python-roborock/python-roborock/commit/e3c419c98f64bc3adada4cc78ce4de366b5267cb)) ## v0.4.14 (2023-04-03) ### Bug Fixes - Adding is_valid function to RoborockBase ([`7575aee`](https://github.com/Python-roborock/python-roborock/commit/7575aeea3b1ca4cfe4a1fb0cb3cea29e964f52b7)) ## v0.4.13 (2023-04-03) ### Bug Fixes - Adiing broken pipe exception log ([`7e73eb2`](https://github.com/Python-roborock/python-roborock/commit/7e73eb2ac7b93f6d0d7331515cf9db5da2c92dc5)) ## v0.4.12 (2023-04-03) ### Bug Fixes - Add containers for dock information ([`77dc414`](https://github.com/Python-roborock/python-roborock/commit/77dc4146b16906807d8a5fbc5025c4a8344c62f0)) ### Chores - Add changelog ([`cc3f378`](https://github.com/Python-roborock/python-roborock/commit/cc3f378d9427c95a66ecdd5c1277a7415e322850)) - Pypi cleanup ([`1878e8e`](https://github.com/Python-roborock/python-roborock/commit/1878e8e42692a2f56679fbdd667da29dfcf759e3)) ## v0.4.11 (2023-04-01) ### Bug Fixes - Changing RoborockDeviceInfo to serializable ([`6dd8ff8`](https://github.com/Python-roborock/python-roborock/commit/6dd8ff8e622d5021e20caf19d36812e34e6c435f)) ## v0.4.10 (2023-04-01) ### Bug Fixes - Using entire object for roborock device info ([`599d461`](https://github.com/Python-roborock/python-roborock/commit/599d461af69c7d6b220973c5d905decc5657ce0f)) ## v0.4.9 (2023-04-01) ### Bug Fixes - Cloud_api.py ([`39fd964`](https://github.com/Python-roborock/python-roborock/commit/39fd964a9ccd0a33310747d6f7d764db1b7c3c23)) ## v0.4.8 (2023-04-01) ### Bug Fixes - Refactor roborock device info ([`291a6b2`](https://github.com/Python-roborock/python-roborock/commit/291a6b295943d6635116e79f7f56c97a553a7c62)) ## v0.4.7 (2023-04-01) ### Bug Fixes - Local_api should receive ip for each device ([`b2f2f15`](https://github.com/Python-roborock/python-roborock/commit/b2f2f1566a27505ebf456aef360b76d001a1351c)) ## v0.4.6 (2023-04-01) ### Bug Fixes - Adding local_api disconnection ([`a010304`](https://github.com/Python-roborock/python-roborock/commit/a01030480353b8d6524c71e463455802082f4066)) - Move add_status_listener from cloud_api to base_api ([`dcad915`](https://github.com/Python-roborock/python-roborock/commit/dcad91545ba18e163ba4ceca887065817b0a4e0c)) ## v0.4.5 (2023-04-01) ### Bug Fixes - Close socket on broken pipe ([`bf8c8d5`](https://github.com/Python-roborock/python-roborock/commit/bf8c8d52b390b27b442a3b7dd046f8ece483bc2e)) ### Chores - Fix cloud_api.py ([`b954c9c`](https://github.com/Python-roborock/python-roborock/commit/b954c9c22977b8239b034e346292a23afe5acbfb)) ## v0.4.4 (2023-04-01) ### Bug Fixes - Removing local_api.py nonworking commands from api.py ([`12bf756`](https://github.com/Python-roborock/python-roborock/commit/12bf756d8d5193bd4cfd9b59d85f11ec3ad4f6e0)) ### Chores - Add new commands ([`e0869cf`](https://github.com/Python-roborock/python-roborock/commit/e0869cf83e87d4c35986acdddf25f650acbd92ee)) - Removing local_api.py nonworking commands from api.py ([`70c04a3`](https://github.com/Python-roborock/python-roborock/commit/70c04a32878cb98c1e009860f2b6d8ede83a6e47)) ## v0.4.3 (2023-04-01) ### Bug Fixes - Minor fixes ([`29bdb45`](https://github.com/Python-roborock/python-roborock/commit/29bdb4542e1c32b956ea8b739f9a610b92e27259)) ## v0.4.2 (2023-04-01) ### Bug Fixes - Refactoring api ([`aa66e1d`](https://github.com/Python-roborock/python-roborock/commit/aa66e1d31ed635690104f9b30b62421e8a2ba663)) ## v0.4.1 (2023-03-31) ### Bug Fixes - Code cleaning ([`d6e3b34`](https://github.com/Python-roborock/python-roborock/commit/d6e3b34bfa5e1803b5e5e494711e56b7d909f1ea)) ## v0.4.0 (2023-03-31) ### Features - Sppliting clients into local and cloud ([`8019313`](https://github.com/Python-roborock/python-roborock/commit/8019313ccb50233610b74d2626ae87e79f55204e)) ## v0.3.1 (2023-03-30) ### Bug Fixes - Minor fixes to offline integration ([`1b4926e`](https://github.com/Python-roborock/python-roborock/commit/1b4926e1d79401f21bee68e4676235426e253191)) ## v0.3.0 (2023-03-30) ### Features - Adding offline.py for others to test local api ([`22680bf`](https://github.com/Python-roborock/python-roborock/commit/22680bfd7929d77b12c27c270478c3253d0cfada)) ## v0.2.3 (2023-03-29) ### Bug Fixes - Bug with dock commands ([`2f2cfb6`](https://github.com/Python-roborock/python-roborock/commit/2f2cfb6b702b6a6f9500e3b272761962ed15ed09)) ## v0.2.2 (2023-03-28) ### Bug Fixes - Change semantic_release from tag_only to tag ([`cad8973`](https://github.com/Python-roborock/python-roborock/commit/cad897381515530ba221b2f92a75ebb3fde876bd)) ## v0.2.1 (2023-03-28) ### Bug Fixes - Repository variable for python-semantic-release ([`b9e21a3`](https://github.com/Python-roborock/python-roborock/commit/b9e21a3d2f5db0a426b96031e154a2a001bc3242)) ## v0.2.0 (2023-03-28) ### Bug Fixes - Add version source ([`c46e503`](https://github.com/Python-roborock/python-roborock/commit/c46e503b91159468e7cf4afb9549c720c1d3dee0)) - Change github token from user defined secret to default secret ([`5886535`](https://github.com/Python-roborock/python-roborock/commit/58865350d583ffa1c4e00a2c22c12b8cf60d3c5f)) - Change to timeout from wait_for ([`eaa4dee`](https://github.com/Python-roborock/python-roborock/commit/eaa4dee1dca696a5817205cd4387b92ce93df0bf)) wait_for creates a task, async_timeout does the same work and avoids the task creation - Removed unneeded line ([`f2b4c89`](https://github.com/Python-roborock/python-roborock/commit/f2b4c89500ac169e9dc021de6e250474f6f75b15)) - Rename github_token to gh_token ([`012cd9d`](https://github.com/Python-roborock/python-roborock/commit/012cd9d0ec065d78063472dc66e60e9545547e24)) - Version source from pyproject.toml ([`20d3c59`](https://github.com/Python-roborock/python-roborock/commit/20d3c59bab6fee2093b892cdc062f929a2b83304)) ### Chores - Add typing to user_data property ([`16f1d5d`](https://github.com/Python-roborock/python-roborock/commit/16f1d5dc10123987ee480bc4696a9a80a5bbe376)) - Added some typing ([`3a72b58`](https://github.com/Python-roborock/python-roborock/commit/3a72b58273d80f0a5d8d8da473e2b0e16aeea722)) - Added typing for containers ([`be20ae1`](https://github.com/Python-roborock/python-roborock/commit/be20ae1fb8c3055b54de083b542cee86874ba9f7)) - Bump pycryptodome to 3.17 ([`1931073`](https://github.com/Python-roborock/python-roborock/commit/193107361f81706e2a67b9558b9e0ad56607166b)) - Bump version ([`33ab4d1`](https://github.com/Python-roborock/python-roborock/commit/33ab4d1523aa21dc692685cd109f878888ee4d78)) - Fix tests with new code mapping ([`4dac8f5`](https://github.com/Python-roborock/python-roborock/commit/4dac8f5ced0dbe0c948a8e8ca335d05f39b27634)) - Moved code mappings to api ([`81bf2e2`](https://github.com/Python-roborock/python-roborock/commit/81bf2e24342dd0b5c1fee3d0c32c38cf4791f7d0)) ### Features - Add dock error mapping ([`4694c66`](https://github.com/Python-roborock/python-roborock/commit/4694c661edaa09a2f637a4ad2191a3b587613ffb)) - Added semantic release ([`2bb2279`](https://github.com/Python-roborock/python-roborock/commit/2bb2279187609a7a7cf4c1a854ede54e8a671860)) - Adding more options to commands ([`9b20345`](https://github.com/Python-roborock/python-roborock/commit/9b203456c3bd5e075e2945be24e1aa65620af12f)) python-roborock-2.19.0/LICENSE000066400000000000000000001045151501065527500157630ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . python-roborock-2.19.0/README.md000066400000000000000000000055451501065527500162400ustar00rootroot00000000000000# Roborock

PyPI Version Supported Python versions License

Roborock library for online and offline control of your vacuums. ## Installation Install this via pip (or your favourite package manager): `pip install python-roborock` ## Functionality You can see all of the commands supported [here](https://python-roborock.readthedocs.io/en/latest/api_commands.html) ## Sending Commands Here is an example that requires no manual intervention and can be done all automatically. You can skip some steps by caching values or looking at them and grabbing them manually. ```python import asyncio from roborock import HomeDataProduct, DeviceData, RoborockCommand from roborock.version_1_apis import RoborockMqttClientV1, RoborockLocalClientV1 from roborock.web_api import RoborockApiClient async def main(): web_api = RoborockApiClient(username="youremailhere") # Login via your password user_data = await web_api.pass_login(password="pass_here") # Or login via a code await web_api.request_code() code = input("What is the code?") user_data = await web_api.code_login(code) # Get home data home_data = await web_api.get_home_data_v2(user_data) # Get the device you want device = home_data.devices[0] # Get product ids: product_info: dict[str, HomeDataProduct] = { product.id: product for product in home_data.products } # Create the Mqtt(aka cloud required) Client device_data = DeviceData(device, product_info[device.product_id].model) mqtt_client = RoborockMqttClientV1(user_data, device_data) networking = await mqtt_client.get_networking() local_device_data = DeviceData(device, product_info[device.product_id].model, networking.ip) local_client = RoborockLocalClientV1(local_device_data) # You can use the send_command to send any command to the device status = await local_client.send_command(RoborockCommand.GET_STATUS) # Or use existing functions that will give you data classes status = await local_client.get_status() asyncio.run(main()) ``` ## Supported devices You can find what devices are supported [here](https://python-roborock.readthedocs.io/en/latest/supported_devices.html). Please note this may not immediately contain the latest devices. ## Credits Thanks @rovo89 for https://gist.github.com/rovo89/dff47ed19fca0dfdda77503e66c2b7c7 And thanks @PiotrMachowski for https://github.com/PiotrMachowski/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor python-roborock-2.19.0/commitlint.config.mjs000066400000000000000000000004411501065527500211050ustar00rootroot00000000000000export default { extends: ["@commitlint/config-conventional"], ignores: [(msg) => /Signed-off-by: dependabot\[bot]/m.test(msg)], rules: { // Disable the rule that enforces lowercase in subject "subject-case": [0], // 0 = disable, 1 = warn, 2 = error }, }; python-roborock-2.19.0/docs/000077500000000000000000000000001501065527500157005ustar00rootroot00000000000000python-roborock-2.19.0/docs/Makefile000066400000000000000000000011761501065527500173450ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) python-roborock-2.19.0/docs/make.bat000066400000000000000000000013741501065527500173120ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd python-roborock-2.19.0/docs/requirements.txt000066400000000000000000000000311501065527500211560ustar00rootroot00000000000000sphinx sphinx_rtd_theme python-roborock-2.19.0/docs/source/000077500000000000000000000000001501065527500172005ustar00rootroot00000000000000python-roborock-2.19.0/docs/source/_templates/000077500000000000000000000000001501065527500213355ustar00rootroot00000000000000python-roborock-2.19.0/docs/source/_templates/footer.html000066400000000000000000000004051501065527500235200ustar00rootroot00000000000000{% extends "!footer.html" %} {%- block contentinfo %} {{ super() }}

We are looking for contributors to help with our documentation, if you are interested please contribute here. {% endblock %} python-roborock-2.19.0/docs/source/api_commands.rst000066400000000000000000001041111501065527500223620ustar00rootroot00000000000000Api commands ============ This page is still under construction. All of the following are the commands we have reverse engineered. It is not an exhaustive list of all the possible commands. Commands do not immediately make it to this page. You can find more commands [here](https://github.com/humbertogontijo/python-roborock/blob/main/roborock/roborock_typing.py#L18) Commands can have multiple parameters that can change from one model to another. * :ref:`app_charge` * :ref:`app_get_dryer_setting` * :ref:`app_get_init_status` * :ref:`app_pause` * :ref:`app_rc_end` * :ref:`app_rc_move` * :ref:`app_rc_start` * :ref:`app_rc_stop` * :ref:`app_segment_clean` * :ref:`app_set_dryer_setting` * :ref:`app_start_collect_dust` * :ref:`app_start_wash` * :ref:`app_start` * :ref:`app_stop_collect_dust` * :ref:`app_stop_wash` * :ref:`app_stop` * :ref:`change_sound_volume` * :ref:`close_dnd_timer` * :ref:`del_server_timer` * :ref:`dnld_install_sound` * :ref:`get_clean_sequence` * :ref:`get_consumable` * :ref:`get_custom_mode` * :ref:`get_customize_clean_mode` * :ref:`get_dnd_timer` * :ref:`get_dust_collection_mode` * :ref:`get_clean_follow_ground_material_status` * :ref:`get_identify_furniture_status` * :ref:`get_identify_ground_material_status` * :ref:`get_led_status` * :ref:`get_map_v1` * :ref:`get_multi_map` * :ref:`get_multi_maps_list` * :ref:`get_network_info` * :ref:`get_prop` * :ref:`get_room_mapping` * :ref:`get_scenes_valid_tids` * :ref:`get_serial_number` * :ref:`get_smart_wash_params` * :ref:`get_sound_progress` * :ref:`get_status` * :ref:`get_timezone` * :ref:`get_turn_server` * :ref:`get_valley_electricity_timer` * :ref:`get_wash_towel_mode` * :ref:`load_multi_map` * :ref:`name_segment` * :ref:`reset_consumable` * :ref:`resume_segment_clean` * :ref:`resume_zoned_clean` * :ref:`retry_request` * :ref:`reunion_scenes` * :ref:`save_map` * :ref:`send_ice_to_robot` * :ref:`send_sdp_to_robot` * :ref:`set_server_timer` * :ref:`set_clean_motor_mode` * :ref:`set_customize_clean_mode` * :ref:`set_dnd_timer` * :ref:`set_dust_collection_mode` * :ref:`set_fds_endpoint` * :ref:`set_identify_furniture_status` * :ref:`set_identify_ground_material_status` * :ref:`set_led_status` * :ref:`set_mop_mode` * :ref:`set_scenes_segments` * :ref:`set_scenes_zones` * :ref:`set_segment_ground_material` * :ref:`set_smart_wash_params` * :ref:`set_timezone` * :ref:`set_valley_electricity_timer` * :ref:`set_wash_towel_mode` * :ref:`set_water_box_custom_mode` * :ref:`start_camera_preview` * :ref:`start_edit_map` * :ref:`start_voice_chat` * :ref:`start_wash_then_charge` * :ref:`stop_camera_preview` * :ref:`stop_segment_clean` * :ref:`test_sound_volume` * :ref:`upd_server_timer` Robot status ------------ get_status ~~~~~~~~~~ Description: Returns the current status of the vacuum Parameters: None Returns: msg_ver: msg_seq: state: battery: Battery level of your device. clean_time: Total clean time in hours. clean_area: Total clean area in meters. error_code: map_reset: in_cleaning: in_returning: in_fresh_state: lab_status: water_box_status: back_type: wash_phase: wash_ready: fan_power: dnd_enabled: map_status: is_locating: lock_status: water_box_mode: water_box_carriage_status: mop_forbidden_enable: camera_status: is_exploring: home_sec_status: home_sec_enable_password: adbumper_status: water_shortage_status: dock_type: dust_collection_status: auto_dust_collection: avoid_count: mop_mode: debug_mode: collision_avoid_status: switch_map_mode: dock_error_status: charge_status: unsave_map_reason: unsave_map_flag: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= App vacuum control ------------------ app_start ~~~~~~~~~ Description: Parameters: app_pause ~~~~~~~~~ Description: This pauses the vacuum's current task Parameters: None Returns ok or error ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= app_stop ~~~~~~~~ Description: Parameters: app_start_collect_dust ~~~~~~~~~~~~~~~~~~~~~~ Description: This empties the bin while docked Parameters: None ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= app_stop_collect_dust ~~~~~~~~~~~~~~~~~~~~~~ Description: This stops the emptying of the dust bin while docked Parameters: None ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= app_start_wash ~~~~~~~~~~~~~~ Description: This washes the mop while docked Parameters: None ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= app_stop_wash ~~~~~~~~~~~~~ Description: This stops washing the mop whiloe docked Parameters: None ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= app_goto_target ~~~~~~~~~~~~~~~ Description: Got to target Parameters: - X coordinate as integer (e.g.: 23450) - Y coordinate as integer (e.g.: 16450) Returns ok or error ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= app_charge ~~~~~~~~~~ Description: This tells your vacuum to go back to the dock and charge. Parameters: None Returns : ok or error ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= App status ---------- app_get_init_status ~~~~~~~~~~~~~~~~~~~ Description: Returns details on the app being used to interact with Roborock servers ?? In this case the app is backend supporting the HA integration ? Parameters: None Returns: local_info: name: Name of the app bom: Version of the app location: Location of the app language: Language of the app wifiplan: Wifi plan of the app timezone: Timezone of the app logserver: Log server of the app featureset: Featureset of the app feature_info: List of features new_feature_info: New feature info Return example:: {'local_info': {'name': 'custom_A.03.0342_CE', 'bom': 'A.03.0342', 'location': 'de', 'language': 'en', 'wifiplan': '', 'timezone': 'Europe/Berlin', 'logserver': 'awsde0.fds.api.xiaomi.com', 'featureset': 3}, 'feature_info': [111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125], 'new_feature_info': 2247395306799103, 'new_feature_info_str': '00000008009EFFFE'} ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= App dryer settings ------------------ app_get_dryer_setting ~~~~~~~~~~~~~~~~~~~~~ Description: Get dock dryer settings. Parameters: None Returns: status: on: cliff_on: cliff_off count: dry_time: Duration dryer remains on in seconds. off: cliff_on: cliff_off: count: Return example:: {'status': 1, 'on': {'cliff_on': 1, 'cliff_off': 1, 'count': 10, 'dry_time': 7200}, 'off': {'cliff_on': 2, 'cliff_off': 1, 'count': 10}} Source: Roborock S7 MaxV Ultra ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= app_set_dryer_setting ~~~~~~~~~~~~~~~~~~~~~ Description: Set the time for the dryer to run Parameters: '{"status":1,"on":{"dry_time":14400}}' dry_time is the time in seconds the dryer will run for Returns ok or error ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= App remote control ------------------ app_rc_start ~~~~~~~~~~~~ Description: Starts remote control. Parameters: None Returns ok or error ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= app_rc_move ~~~~~~~~~~~ Description: Moves the robot in the direction specified Parameters: To be documented Returns ok or error .. Need to document the parameters - will need to explore the app to find out what they are app_rc_stop ~~~~~~~~~~~ Description: Stops the remote control Parameters: None Returns ok or error .. Assume stop stops a move ?? Need to check app_rc_end ~~~~~~~~~~ Description: Ends the remote control task Parameters: Returns ok or error ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= App other --------- app_set_smart_cliff_forbidden ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: app_spot ~~~~~~~~ Description: Parameters: app_stat ~~~~~~~~ Description: This returns the current status of the vacuum Parameters: None Returns: ok or error ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= app_wakeup_robot ~~~~~~~~~~~~~~~~ Description: Parameters: app_zoned_clean ~~~~~~~~~~~~~~~ Description: Starts a zone clean Parameters: .. Us this the last known zone Returns: ok or error ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= Segments and Zones ------------------ app_segment_clean ~~~~~~~~~~~~~~~~~ Description: This starts a segment clean and repeats it the number of times specified. Parameters: An array of segments to clean. Each segment is an integer with the segment id and the number of times to clean it. For example, to clean segment 18 twice, the parameter would be [{'segments': [18], 'repeat': 2}] .. Comment: The segment id can be obtained from the initial data returned on login Command: roborock -d command --device_id deviceIdRedacted --cmd app_segment_clean --params '[{"segments": [17,19], "repeat": 2}]' Returns ok or error ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= set_segment_ground_material ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Sets the groud material for the segment Parameters: "{'data':[[22,3,0]]}" Returns ok or error name_segment ~~~~~~~~~~~~ Description: Parameters: To be determined .. Need to work out parameter format Does this allow us to name a segment ? resume_segment_clean ~~~~~~~~~~~~~~~~~~~~ Description: Parameters: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= stop_segment_clean ~~~~~~~~~~~~~~~~~~ Description: Parameters: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= set_scenes_zones ~~~~~~~~~~~~~~~~ Description: Parameters: set_scenes_segments ~~~~~~~~~~~~~~~~~~~ Description: Parameters: get_scenes_valid_tids ~~~~~~~~~~~~~~~~~~~~~ Description: To be confirmed Parameters: None .. Appears to be associated with rooms ?? Returns:: [{'tid': '1699679077347', 'map_flag': 0, 'segs': [{'sid': 24}, {'sid': 20}, {'sid': 22}, {'sid': 18}]}, {'tid': '1699679236553', 'map_flag': 0, 'segs': [{'sid': 24}, {'sid': 20}, {'sid': 22}]}, {'tid': '1699679386045', 'map_flag': 0, 'segs': [{'sid': 16}, {'sid': 19}, {'sid': 17}]}, {'tid': '1699679335823', 'map_flag': 0, 'segs': [{'sid': 19}, {'sid': 16}, {'sid': 17}]}] ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= resume_zoned_clean ~~~~~~~~~~~~~~~~~~ Description: Parameters: reunion_scenes ~~~~~~~~~~~~~~ Description: Parameters: Camera ------ start_camera_preview ~~~~~~~~~~~~~~~~~~~~ Description: Parameters: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra No ====================== ========= stop_camera_preview ~~~~~~~~~~~~~~~~~~~ Description: Parameters: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra No ====================== ========= get_camera_status ~~~~~~~~~~~~~~~~~ Description: Get camera status. Parameters: None Returns: 3457  387 Roborock S8 Pro Ultra Source: Roborock S7 MaxV Ultra set_camera_status ~~~~~~~~~~~~~~~~~ Description: Parameters: start_voice_chat ~~~~~~~~~~~~~~~~ Description: Parameters: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra No ====================== ========= Clean modes ----------------- get_carpet_clean_mode ~~~~~~~~~~~~~~~~~~~~~ Description: Get carpet clean mode. Parameters: Returns: carpet_clean_mode: Enumeration for carpet clean mode. Return example:: {'carpet_clean_mode': 3} Source: Roborock S7 MaxV Ultra set_carpet_clean_mode ~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: get_carpet_mode ~~~~~~~~~~~~~~~ Description: Parameters: None Returns: enable: current_integral: current_high: current_low: stall_time: Return example:: {'enable': 1, 'current_integral': 450, 'current_high': 500, 'current_low': 400, 'stall_time': 10} ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= set_carpet_mode ~~~~~~~~~~~~~~~~~~~~ Description: Parameters: get_smart_wash_params ~~~~~~~~~~~~~~~~~~~~~ Description: Returns the smartwash parameters Parameters: None .. Not clear what this does Returns: smart_wash: 0 is off, 1 is on wash_interval: The interval in seconds between washes Example:: {'smart_wash': 0, 'wash_interval': 1200} ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= set_smart_wash_params ~~~~~~~~~~~~~~~~~~~~~ Description: Sets the smartwash parameters Parameters: smart_wash: 0 is off, 1 is on wash_interval: The interval in seconds between washes {'smart_wash': 0, 'wash_interval': 1200} ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= Cleaning history ---------------- get_clean_record ~~~~~~~~~~~~~~~~ Description: Parameters: To be determined get_clean_record_map ~~~~~~~~~~~~~~~~~~~~ Description: Parameters: get_clean_sequence ~~~~~~~~~~~~~~~~~~ Description: Parameters: get_clean_summary ~~~~~~~~~~~~~~~~~ Description: Get a summary of cleaning history. Parameters: None Returns: clean_time: clean_area: clean_count: dust_collection_count: records: Return example:: {'clean_time': 568146, 'clean_area': 8816865000, 'clean_count': 178, 'dust_collection_count': 172, 'records': [1689740211, 1689555788, 1689259450, 1688999113, 1688852350, 1688693213, 1688692357, 1688614354, 1688613280, 1688606676, 1688325265, 1688174717, 1688149381, 1688092832, 1688001593, 1687921414, 1687890618, 1687743256, 1687655018, 1687631444]} Source: Roborock S7 MaxV Ultra ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= get_mop_template_params_summary ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra No ====================== ========= Child lock ---------- get_child_lock_status ~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: This gets the child lock status of the device. 0 is off, 1 is on. Parameters: None Returns: lock_status: Return example:: {'lock_status': 0} ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= set_child_lock_status ~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: This sets the child lock status of the device. Parameters: '{"lock_status" :0}' Returns: ok ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= Consumables ----------- get_consumable ~~~~~~~~~~~~~~ Description: This gets the status of all of the consumables for your device. Parameters: None Returns: main_brush_work_time: This is the amount of time the main brush has been used in seconds since it was last replaced side_brush_work_time: This is the amount of time the side brush has been used in seconds since it was last replaced filter_work_time: This is the amount of time the air filter inside the vacuum has been used in seconds since it was last replaced filter_element_work_time: sensor_dirty_time: This is the amount of time since you have cleaned the sensors on the bottom of your vacuum. strainer_work_times: dust_collection_work_times: cleaning_brush_work_times: Return examples:: {'main_brush_work_time': 14151, 'side_brush_work_time': 41638, 'filter_work_time': 14151, 'filter_element_work_time': 0, 'sensor_dirty_time': 41522, 'strainer_work_times': 44, 'dust_collection_work_times': 19, 'cleaning_brush_work_times': 44} reset_consumable ~~~~~~~~~~~~~~~~ Description: Parameters: List of consumables to reset. For example, to reset consumables 'strainer_work_times' and 'sensor_dirty_time' the parameter would be ['strainer_work_times', 'sensor_dirty_time'] ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= Custom modes ------------ get_custom_mode ~~~~~~~~~~~~~~~~~~~~ Description: It returns the current custom mode. Parameters: None Returns: integer value of the current custom mode Return example:: 102 .. Not clear what a custom mode is = will explore ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= set_custom_mode ~~~~~~~~~~~~~~~~~~~~ Description: Parameters: get_customize_clean_mode ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: set_customize_clean_mode ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: Furniture and ground material ----------------------------- get_identify_furniture_status ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: .. Does not return anything for S8 Pro Ultra when docked may require vacumm to be cleaning set_identify_furniture_status ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: .. Method not known for S8 Pro Ultra get_identify_ground_material_status ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: .. Does not return anything for S8 Pro Ultra when docked may require vacumm to be cleaning set_identify_ground_material_status ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: .. Method not known for S8 Pro Ultra LEDs ---- get_flow_led_status ~~~~~~~~~~~~~~~~~~~ Description: Parameters: set_flow_led_status ~~~~~~~~~~~~~~~~~~~ Description: Parameters: get_led_status ~~~~~~~~~~~~~~~~~~~ Description: Returns the LED status. If disabled the indicator light will turn off 1 minute after fully charged Parameters: Returns: led_status: 0 is off, 1 is on ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= set_led_status ~~~~~~~~~~~~~~ Description: Sets the LED status. If disabled the indicator light will turn off 1 minute after fully charged Parameters: ???? .. Need to work out parameter format Maps ---- get_multi_map ~~~~~~~~~~~~~ Description: Parameters: Comment: Response timed out for S8 Pro Ultra .. times out after 4 secs get_multi_maps_list ~~~~~~~~~~~~~~~~~~~ Description: Returns a list of map information stored on the device. Parameters: None required Returns: max_multi_map: max_bak_map: multi_map_count: map_info:: mapFlag: add_time: length: name: bak_maps:: mapFlag: add_time: Return example:: {'max_multi_map': 4, 'max_bak_map': 1, 'multi_map_count': 2, 'map_info': [{'mapFlag': 0, 'add_time': 1699919699, 'length': 4, 'name': 'Home', 'bak_maps': [{'mapFlag': 4, 'add_time': 1699823921}]}, {'mapFlag': 1, 'add_time': 1699828035, 'length': 13, 'name': 'Boys bathroom', 'bak_maps': [{'mapFlag': 5, 'add_time': 1699828035}]}]} Source: S8 Pro Ultra ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= get_map_v1 ~~~~~~~~~~ Description: Returns the map Parameters: Unknown Comment: Returns a map in a format that is not yet understood by me .. Explore what parameters it may take Extend code to return byte stream ? start_edit_map ~~~~~~~~~~~~~~ Description: Parameters: get_room_mapping ~~~~~~~~~~~~~~~~ Description: Returns a list of rooms, ids as discovered by Parameters: None Returns: room_id Return example:: [[16, '14731399', 12], [17, '2220009', 2], [18, '2219688', 12], [19, '2219685', 9], [20, '2219691', 12], [21, '2431758', 12], [22, '2219677', 13], [23, '2312548', 12], [24, '2219678', 14], [25, '2219686', 15], [26, '2219772', 12], [27, '14768755', 12]] ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= load_multi_map ~~~~~~~~~~~~~~ Description: Parameters: number (the floor/map index) .. Need to work out parameter format save_map ~~~~~~~~ Description: Parameters: Operating modes --------------- get_mop_mode ~~~~~~~~~~~~ Description: Get mop mode. Parameters: None Returns: Enumeration for mop mode. 300 Example for S8 Pro Ultra:: standard = 300 deep = 301 deep_plus = 303 fast = 304 custom = 302 ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= set_mop_mode ~~~~~~~~~~~~ Description: Set mop mode. Parameters: mop_mode 300 ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= set_clean_motor_mode ~~~~~~~~~~~~~~~~~~~~ Description: Parameters: get_dust_collection_mode ~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: None Returns: mode: Return example:: {'mode': 0} Source: Roborock S7 MaxV Ultra ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= set_dust_collection_mode ~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: get_wash_towel_mode ~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: None Returns: wash_mode: Return example:: {'wash_mode': 1} Source: Roborock S7 MaxV Ultra unknown = -9999 light = 0 balanced = 1 deep = 2 ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= set_wash_towel_mode ~~~~~~~~~~~~~~~~~~~~~~~~ Description: Sets the wash wash_towel_mode Parameters: {'wash_mode': 2} Returns: ok or error Source: S8 Pro Ultra ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= get_collision_avoid_status ~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: None Returns: status: Return example:: {'status': 1} ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= set_collision_avoid_status ~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Update collision avoid status. Parameters: '{"status" :1}' Returns: ok ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= start_wash_then_charge ~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: .. While this returns ok on the S8 Pro Ultra it does not appear to do anything switch_water_mark ~~~~~~~~~~~~~~~~~ Description: Parameters: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra No ====================== ========= .. Not found for S8 Pro Ultra System information ------------------ get_network_info ~~~~~~~~~~~~~~~~ Description: Get the device's network information. Parameters: None Returns: ssid: SSID of the wirelness network the device is connected to. ip: IP address of the device. mac: MAC address of the device. bssid: BSSID of the device. rssi: RSSI of the device. Return example:: {'ssid': 'My WiFi Network', 'ip': '192.168.1.29', 'mac': 'a0:2b:47:3d:24:51', 'bssid': '18:3b:1a:23:41:3c', 'rssi': -32} Source: Roborock S7 MaxV Ultra ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= get_serial_number ~~~~~~~~~~~~~~~~~ Description: Get serial number of the vacuum. Parameters: None Returns:: serial_number: Serial number of the vacuum. Return example:: {'serial_number': 'B16EVD12345678'} Source: Roborock S7 MaxV Ultra ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= get_prop ~~~~~~~~ Description: Generic get property command Parameters: The property to get Example:: roborock -d command --device_id aHiddenDeviceId --cmd get_prop --params '["battery"]' Comment : This example returns the same as get_status. Initial testing has shown that not all get commands are supported by this method get_turn_server ~~~~~~~~~~~~~~~ Description: Parameters: .. Not found for S8 Pro Ultra ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra No ====================== ========= enable_log_upload ~~~~~~~~~~~~~~~~~ Description: Parameters: find_me ~~~~~~~ Description: This makes your vacuum speak so you can find it. Parameters: None upd_server_timer ~~~~~~~~~~~~~~~~ Description: Parameters: get_homesec_connect_status ~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra No ====================== ========= set_fds_endpoint ~~~~~~~~~~~~~~~~ Description: Parameters: send_ice_to_robot ~~~~~~~~~~~~~~~~~ Description: Parameters: send_sdp_to_robot ~~~~~~~~~~~~~~~~~ Description: Parameters: get_device_ice ~~~~~~~~~~~~~~ .. This doeas not appear to be supported on S8 Pro Ultra Description: Parameters: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra No ====================== ========= get_device_sdp ~~~~~~~~~~~~~~ Description: Parameters: ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra No ====================== ========= retry_request ~~~~~~~~~~~~~ Description: Parameters: Timers ------ del_server_timer ~~~~~~~~~~~~~~~~ Description: Parameters: dnd_timer ~~~~~~~~~ get_dnd_timer ~~~~~~~~~~~~~ Description: Gets the do not disturb timer start_hour: The hour you want dnd to start start_minute: The minute you want dnd to start end_hour: The hour you want dnd to be turned off end_minute: The minute you want dnd to be turned off enabled: If the switch is currently turned on in the app for DnD Parameters: None set_dnd_timer ~~~~~~~~~~~~~ Description: Parameters: close_dnd_timer ~~~~~~~~~~~~~~~ Description: This disables the dnd timer Parameters: None get_server_timer ~~~~~~~~~~~~~~~~ Description: Parameters: set_server_timer ~~~~~~~~~~~~~~~~ Description: Parameters: get_timezone ~~~~~~~~~~~~~~~~~ Description: Get the device's time zone. Parameters: None Returns: Time zone by the TZ identifier (e.g., America/Los_Angeles) ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= set_timezone ~~~~~~~~~~~~~~~~~ Description: Sets the device's time zone Parameters: Sound ------------ get_sound_volume ~~~~~~~~~~~~~~~~ Description: Returns the volume of the sound played by the vacuum Parameters: None Returns: volume: The volume of the sound played by the vacuum Example:: 72 ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= change_sound_volume ~~~~~~~~~~~~~~~~~~~ Description: Sets the volume of the sound played by the vacuum Parameters: volume Returns: ok or error roborock -d command --device_id aHiddenDeviceId --cmd change_sound_volume --params 72 ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= test_sound_volume ~~~~~~~~~~~~~~~~~ Description: Plays a sound on the vacumm to identity volume Parameters: None ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= get_sound_progress ~~~~~~~~~~~~~~~~~~ Description: Parameters: Returns:: {'sid_in_progress': 0, 'progress': 0, 'state': 0, 'error': 0} .. Is this where the vacumm is currently located ? get_current_sound ~~~~~~~~~~~~~~~~~ .. Is this an app setting ? Description: Parameters: Return example:: {'sid_in_use': 122, 'sid_version': 1, 'sid_in_progress': 0, 'location': 'de', 'bom': 'A.03.0342', 'language': 'en', 'msg_ver': 2} ====================== ========= Vacuum Model Supported ====================== ========= Roborock S7 MaxV Ultra Yes Roborock S8 Pro Ultra Yes ====================== ========= dnld_install_sound ~~~~~~~~~~~~~~~~~~ Description: Parameters: Off peak charging ----------------- get_valley_electricity_timer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Get valley electricity timer. Parameters: None Returns: start_hour: The hour you want valley electricity to start start_minute: The minute you want valley electricity to start end_hour: The hour you want valley electricity to be turned off end_minute: The minute you want valley electricity to be turned off enabled: If the switch is currently turned on in the app for valley electricity ``` {'start_hour': 0, 'start_minute': 0, 'end_hour': 0, 'end_minute': 0, 'enabled': 0} ``` ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= set_valley_electricity_timer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Sets the valley electricity timer Parameters: start_hour: The hour you want valley electricity to start start_minute: The minute you want valley electricity to start end_hour: The hour you want valley electricity to be turned off end_minute: The minute you want valley electricity to be turned off enabled: If the switch is currently turned on in the app for valley electricity Example:: {'start_hour': 0, 'start_minute': 0, 'end_hour': 0, 'end_minute': 0, 'enabled': 0} .. This does not appear to have any effect on the S8 Pro Ultra - Params accepted however no affect ?? ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra ??? ====================== ========= Water box mode -------------- get_water_box_custom_mode ~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Get water box mode. Parameters: None Returns: Enumeration for water box mode. 203 .. Not clear what this does - require Enumeration get_clean_follow_ground_material_status ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Parameters: None ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= set_water_box_custom_mode ~~~~~~~~~~~~~~~~~~~~~~~~~ Description: Set the water box mode. Parameters: {'water_box_mode': 203} Returns: ok or error .. Not clear what this does - require Enumeration ====================== ========= Vacuum Model Supported ====================== ========= Roborock S8 Pro Ultra Yes ====================== ========= python-roborock-2.19.0/docs/source/conf.py000066400000000000000000000013751501065527500205050ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # -- Project information project = "Python Roborock" author = "Humberto gontijo & Lash-L" release = "0.1" version = "0.1.0" # -- General configuration extensions = [ "sphinx.ext.duration", "sphinx.ext.doctest", "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.intersphinx", "sphinx.ext.autosectionlabel", "sphinx_rtd_theme", ] intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "sphinx": ("https://www.sphinx-doc.org/en/master/", None), } intersphinx_disabled_domains = ["std"] templates_path = ["_templates"] # -- Options for HTML output html_theme = "sphinx_rtd_theme" # -- Options for EPUB output epub_show_urls = "footnote" python-roborock-2.19.0/docs/source/error.rst000066400000000000000000000021031501065527500210570ustar00rootroot00000000000000Error ===== Dock Errors ----------- These are the potential errors your dock can have and their corresponding number: ok = 0 duct_blockage = 34 water_empty = 38 waste_water_tank_full = 39 dirty_tank_latch_open = 44 no_dustbin = 46 cleaning_tank_full_or_blocked = 53 Vacuum Errors ------------- These are the potential errors your vacuum can have and their corresponding code lidar_blocked = 1 bumper_stuck = 2 wheels_suspended = 3 cliff_sensor_error = 4 main_brush_jammed = 5 side_brush_jammed = 6 wheels_jammed = 7 robot_trapped = 8 no_dustbin = 9 low_battery = 12 charging_error = 13 battery_error = 14 wall_sensor_dirty = 15 robot_tilted = 16 side_brush_error = 17 fan_error = 18 vertical_bumper_pressed = 21 dock_locator_error = 22 return_to_dock_fail = 23 nogo_zone_detected = 24 vibrarise_jammed = 27 robot_on_carpet = 28 filter_blocked = 29 invisible_wall_detected = 30 cannot_cross_carpet = 31 internal_error = 32 python-roborock-2.19.0/docs/source/index.rst000066400000000000000000000010141501065527500210350ustar00rootroot00000000000000Welcome to Roborock's documentation! ==================================== **Roborock** is a Python library for controlling your Roborock vacuum .. note:: This project is under active development. You can get a Home Assistant integration for Roborock in core `here `__ or as a custom integration `here `__ Contents -------- .. toctree:: usage status error api_commands supported_devices python-roborock-2.19.0/docs/source/status.rst000066400000000000000000000030601501065527500212540ustar00rootroot00000000000000Status ====== Status is a core piece of information for our system. It is used to get a wide variety of data about the vacuum and is broadcasted. msg_ver: msg_seq: state: battery: The battery percentage of the vacuum clean_time: How long (total) this vacuum has cleaned for clean_area: How much area (total) this vacuum has cleaned in micrometers error_code: The error code of the vacuum map_present: in_cleaning: If the vacuum is currently cleaning in_returning: If the vacuum is currently returning to the dock. in_fresh_state: lab_status: water_box_status: back_type: wash_phase: wash_ready: fan_power: The strength of the fan suction. Listed as an integer that corresponds to a enum value. dnd_enabled: 0 or 1 that states if there is a dnd time enabled (does not mean that dnd is on now) map_status: is_locating: lock_status: water_box_mode: water_box_carriage_status: mop_forbidden_enable: camera_status: is_exploring: home_sec_status: home_sec_enable_password: adbumper_status: water_shortage_status: dock_type: dust_collection_status: auto_dust_collection: avoid_count: mop_mode: debug_mode: collision_avoid_status: switch_map_mode: dock_error_status: charge_status: unsave_map_reason: unsave_map_flag: wash_status: distance_off: in_warmup: dry_status: rdt: clean_percent: rss: dss: common_status: corner_clean_mode: python-roborock-2.19.0/docs/source/supported_devices.rst000066400000000000000000000043261501065527500234660ustar00rootroot00000000000000Supported Devices ================== Note: These links are tracking links with Amazon or Roborock. This allows us to get some analytics and helps us get 'negotiation' power with Roborock. We would like to be able to open a channel of communication with Roborock, and getting information like this is a great first step. Note, I have only added links to the new devices, older devices are no longer sold directly by roborock, so to buy them you have to find them used. .. list-table:: Robot Vacuums :widths: 30 20 20 :header-rows: 1 * - Vacuum Model - Amazon - Roborock * - Roborock S4 - - * - Roborock S4 Max - - * - Roborock S5 Max - - * - Roborock S6 - - * - Roborock S6 Pure - - * - Roborock S6 Max - - * - Roborock S6 MaxV - - * - Roborock S7 - - * - Roborock S7 MaxV - - * - Roborock S7 Max Ultra - `Link `__ - `Link `__ * - Roborock S8 - `Link `__ - `Link `__ * - Roborock S8 Pro Ultra - `Link `__ - `Link `__ * - Roborock Q5 - `Link `__ - `Link `__ * - Roborock Q5 Pro - `Link `__ - `Link `__ * - Roborock Q7 - `Link `__ - `Link `__ * - Roborock Q7 Max - `Link `__ - `Link `__ * - Roborock Q8 Max - `Link `__ - `Link `__ * - Roborock Q Revo - `Link `__ - `Link `__ Roborock has recently added two other categories of devices, handheld vacuums, and washing machines. Neither are supported at this time. There are plans to support the handheld ones, but it uses a newer version of the api that I am still trying to reverse engineer. python-roborock-2.19.0/docs/source/usage.rst000077500000000000000000000012751501065527500210460ustar00rootroot00000000000000Usage ===== Installation ------------ To use Python-Roborock, first install it using pip: .. code-block:: console (.venv) $ pip install python-roborock Login ----- .. code-block:: console (.venv) $ roborock login --email username --password password List devices ------------ This will list all devices associated with the account: .. code-block:: console (.venv) $ roborock list-devices Known devices MyRobot: 7kI9d66UoPXd6sd9gfd75W The deviceId 7kI9d66UoPXd6sd9gfd75W can be used to run commands on the device. Run a command ------------- To run a command: .. code-block:: console (.venv) $ roborock -d command --device_id 7kI9d66UoPXd6sd9gfd75W --cmd get_status python-roborock-2.19.0/mypy.ini000066400000000000000000000001211501065527500164410ustar00rootroot00000000000000[mypy] check_untyped_defs = True [mypy-construct] ignore_missing_imports = True python-roborock-2.19.0/poetry.lock000066400000000000000000004530161501065527500171550ustar00rootroot00000000000000# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" version = "2.6.1" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, ] [[package]] name = "aiohttp" version = "3.11.16" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "aiohttp-3.11.16-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb46bb0f24813e6cede6cc07b1961d4b04f331f7112a23b5e21f567da4ee50aa"}, {file = "aiohttp-3.11.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:54eb3aead72a5c19fad07219acd882c1643a1027fbcdefac9b502c267242f955"}, {file = "aiohttp-3.11.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:38bea84ee4fe24ebcc8edeb7b54bf20f06fd53ce4d2cc8b74344c5b9620597fd"}, {file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0666afbe984f6933fe72cd1f1c3560d8c55880a0bdd728ad774006eb4241ecd"}, {file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba92a2d9ace559a0a14b03d87f47e021e4fa7681dc6970ebbc7b447c7d4b7cd"}, {file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ad1d59fd7114e6a08c4814983bb498f391c699f3c78712770077518cae63ff7"}, {file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b88a2bf26965f2015a771381624dd4b0839034b70d406dc74fd8be4cc053e3"}, {file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:576f5ca28d1b3276026f7df3ec841ae460e0fc3aac2a47cbf72eabcfc0f102e1"}, {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a2a450bcce4931b295fc0848f384834c3f9b00edfc2150baafb4488c27953de6"}, {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:37dcee4906454ae377be5937ab2a66a9a88377b11dd7c072df7a7c142b63c37c"}, {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4d0c970c0d602b1017e2067ff3b7dac41c98fef4f7472ec2ea26fd8a4e8c2149"}, {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:004511d3413737700835e949433536a2fe95a7d0297edd911a1e9705c5b5ea43"}, {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c15b2271c44da77ee9d822552201180779e5e942f3a71fb74e026bf6172ff287"}, {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad9509ffb2396483ceacb1eee9134724443ee45b92141105a4645857244aecc8"}, {file = "aiohttp-3.11.16-cp310-cp310-win32.whl", hash = "sha256:634d96869be6c4dc232fc503e03e40c42d32cfaa51712aee181e922e61d74814"}, {file = "aiohttp-3.11.16-cp310-cp310-win_amd64.whl", hash = "sha256:938f756c2b9374bbcc262a37eea521d8a0e6458162f2a9c26329cc87fdf06534"}, {file = "aiohttp-3.11.16-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8cb0688a8d81c63d716e867d59a9ccc389e97ac7037ebef904c2b89334407180"}, {file = "aiohttp-3.11.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ad1fb47da60ae1ddfb316f0ff16d1f3b8e844d1a1e154641928ea0583d486ed"}, {file = "aiohttp-3.11.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df7db76400bf46ec6a0a73192b14c8295bdb9812053f4fe53f4e789f3ea66bbb"}, {file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc3a145479a76ad0ed646434d09216d33d08eef0d8c9a11f5ae5cdc37caa3540"}, {file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d007aa39a52d62373bd23428ba4a2546eed0e7643d7bf2e41ddcefd54519842c"}, {file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6ddd90d9fb4b501c97a4458f1c1720e42432c26cb76d28177c5b5ad4e332601"}, {file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a2f451849e6b39e5c226803dcacfa9c7133e9825dcefd2f4e837a2ec5a3bb98"}, {file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8df6612df74409080575dca38a5237282865408016e65636a76a2eb9348c2567"}, {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78e6e23b954644737e385befa0deb20233e2dfddf95dd11e9db752bdd2a294d3"}, {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:696ef00e8a1f0cec5e30640e64eca75d8e777933d1438f4facc9c0cdf288a810"}, {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3538bc9fe1b902bef51372462e3d7c96fce2b566642512138a480b7adc9d508"}, {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3ab3367bb7f61ad18793fea2ef71f2d181c528c87948638366bf1de26e239183"}, {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:56a3443aca82abda0e07be2e1ecb76a050714faf2be84256dae291182ba59049"}, {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:61c721764e41af907c9d16b6daa05a458f066015abd35923051be8705108ed17"}, {file = "aiohttp-3.11.16-cp311-cp311-win32.whl", hash = "sha256:3e061b09f6fa42997cf627307f220315e313ece74907d35776ec4373ed718b86"}, {file = "aiohttp-3.11.16-cp311-cp311-win_amd64.whl", hash = "sha256:745f1ed5e2c687baefc3c5e7b4304e91bf3e2f32834d07baaee243e349624b24"}, {file = "aiohttp-3.11.16-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:911a6e91d08bb2c72938bc17f0a2d97864c531536b7832abee6429d5296e5b27"}, {file = "aiohttp-3.11.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac13b71761e49d5f9e4d05d33683bbafef753e876e8e5a7ef26e937dd766713"}, {file = "aiohttp-3.11.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd36c119c5d6551bce374fcb5c19269638f8d09862445f85a5a48596fd59f4bb"}, {file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d489d9778522fbd0f8d6a5c6e48e3514f11be81cb0a5954bdda06f7e1594b321"}, {file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69a2cbd61788d26f8f1e626e188044834f37f6ae3f937bd9f08b65fc9d7e514e"}, {file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd464ba806e27ee24a91362ba3621bfc39dbbb8b79f2e1340201615197370f7c"}, {file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce63ae04719513dd2651202352a2beb9f67f55cb8490c40f056cea3c5c355ce"}, {file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b00dd520d88eac9d1768439a59ab3d145065c91a8fab97f900d1b5f802895e"}, {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f6428fee52d2bcf96a8aa7b62095b190ee341ab0e6b1bcf50c615d7966fd45b"}, {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13ceac2c5cdcc3f64b9015710221ddf81c900c5febc505dbd8f810e770011540"}, {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fadbb8f1d4140825069db3fedbbb843290fd5f5bc0a5dbd7eaf81d91bf1b003b"}, {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6a792ce34b999fbe04a7a71a90c74f10c57ae4c51f65461a411faa70e154154e"}, {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f4065145bf69de124accdd17ea5f4dc770da0a6a6e440c53f6e0a8c27b3e635c"}, {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa73e8c2656a3653ae6c307b3f4e878a21f87859a9afab228280ddccd7369d71"}, {file = "aiohttp-3.11.16-cp312-cp312-win32.whl", hash = "sha256:f244b8e541f414664889e2c87cac11a07b918cb4b540c36f7ada7bfa76571ea2"}, {file = "aiohttp-3.11.16-cp312-cp312-win_amd64.whl", hash = "sha256:23a15727fbfccab973343b6d1b7181bfb0b4aa7ae280f36fd2f90f5476805682"}, {file = "aiohttp-3.11.16-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a3814760a1a700f3cfd2f977249f1032301d0a12c92aba74605cfa6ce9f78489"}, {file = "aiohttp-3.11.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b751a6306f330801665ae69270a8a3993654a85569b3469662efaad6cf5cc50"}, {file = "aiohttp-3.11.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad497f38a0d6c329cb621774788583ee12321863cd4bd9feee1effd60f2ad133"}, {file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca37057625693d097543bd88076ceebeb248291df9d6ca8481349efc0b05dcd0"}, {file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5abcbba9f4b463a45c8ca8b7720891200658f6f46894f79517e6cd11f3405ca"}, {file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f420bfe862fb357a6d76f2065447ef6f484bc489292ac91e29bc65d2d7a2c84d"}, {file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ede86453a6cf2d6ce40ef0ca15481677a66950e73b0a788917916f7e35a0bb"}, {file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fdec0213244c39973674ca2a7f5435bf74369e7d4e104d6c7473c81c9bcc8c4"}, {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72b1b03fb4655c1960403c131740755ec19c5898c82abd3961c364c2afd59fe7"}, {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:780df0d837276276226a1ff803f8d0fa5f8996c479aeef52eb040179f3156cbd"}, {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ecdb8173e6c7aa09eee342ac62e193e6904923bd232e76b4157ac0bfa670609f"}, {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6db7458ab89c7d80bc1f4e930cc9df6edee2200127cfa6f6e080cf619eddfbd"}, {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2540ddc83cc724b13d1838026f6a5ad178510953302a49e6d647f6e1de82bc34"}, {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b4e6db8dc4879015b9955778cfb9881897339c8fab7b3676f8433f849425913"}, {file = "aiohttp-3.11.16-cp313-cp313-win32.whl", hash = "sha256:493910ceb2764f792db4dc6e8e4b375dae1b08f72e18e8f10f18b34ca17d0979"}, {file = "aiohttp-3.11.16-cp313-cp313-win_amd64.whl", hash = "sha256:42864e70a248f5f6a49fdaf417d9bc62d6e4d8ee9695b24c5916cb4bb666c802"}, {file = "aiohttp-3.11.16-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bbcba75fe879ad6fd2e0d6a8d937f34a571f116a0e4db37df8079e738ea95c71"}, {file = "aiohttp-3.11.16-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:87a6e922b2b2401e0b0cf6b976b97f11ec7f136bfed445e16384fbf6fd5e8602"}, {file = "aiohttp-3.11.16-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccf10f16ab498d20e28bc2b5c1306e9c1512f2840f7b6a67000a517a4b37d5ee"}, {file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb3d0cc5cdb926090748ea60172fa8a213cec728bd6c54eae18b96040fcd6227"}, {file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d07502cc14ecd64f52b2a74ebbc106893d9a9717120057ea9ea1fd6568a747e7"}, {file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:776c8e959a01e5e8321f1dec77964cb6101020a69d5a94cd3d34db6d555e01f7"}, {file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0902e887b0e1d50424112f200eb9ae3dfed6c0d0a19fc60f633ae5a57c809656"}, {file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e87fd812899aa78252866ae03a048e77bd11b80fb4878ce27c23cade239b42b2"}, {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0a950c2eb8ff17361abd8c85987fd6076d9f47d040ebffce67dce4993285e973"}, {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:c10d85e81d0b9ef87970ecbdbfaeec14a361a7fa947118817fcea8e45335fa46"}, {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7951decace76a9271a1ef181b04aa77d3cc309a02a51d73826039003210bdc86"}, {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14461157d8426bcb40bd94deb0450a6fa16f05129f7da546090cebf8f3123b0f"}, {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9756d9b9d4547e091f99d554fbba0d2a920aab98caa82a8fb3d3d9bee3c9ae85"}, {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:87944bd16b7fe6160607f6a17808abd25f17f61ae1e26c47a491b970fb66d8cb"}, {file = "aiohttp-3.11.16-cp39-cp39-win32.whl", hash = "sha256:92b7ee222e2b903e0a4b329a9943d432b3767f2d5029dbe4ca59fb75223bbe2e"}, {file = "aiohttp-3.11.16-cp39-cp39-win_amd64.whl", hash = "sha256:17ae4664031aadfbcb34fd40ffd90976671fa0c0286e6c4113989f78bebab37a"}, {file = "aiohttp-3.11.16.tar.gz", hash = "sha256:16f8a2c9538c14a557b4d309ed4d0a7c60f0253e8ed7b6c9a2859a7582f8b1b8"}, ] [package.dependencies] aiohappyeyeballs = ">=2.3.0" aiosignal = ">=1.1.2" attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiomqtt" version = "2.4.0" description = "The idiomatic asyncio MQTT client" optional = false python-versions = "<4.0,>=3.8" groups = ["main"] files = [ {file = "aiomqtt-2.4.0-py3-none-any.whl", hash = "sha256:721296e2b79df5f6c7c4dfc91700ae0166953a4127735c92637859619dbd84e4"}, {file = "aiomqtt-2.4.0.tar.gz", hash = "sha256:ab0f18fc5b7ffaa57451c407417d674db837b00a9c7d953cccd02be64f046c17"}, ] [package.dependencies] paho-mqtt = ">=2.1.0,<3.0.0" [[package]] name = "aioresponses" version = "0.7.8" description = "Mock out requests made by ClientSession from aiohttp package" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94"}, {file = "aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11"}, ] [package.dependencies] aiohttp = ">=3.3.0,<4.0.0" packaging = ">=22.0" [[package]] name = "aiosignal" version = "1.3.2" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, ] [package.dependencies] frozenlist = ">=1.1.0" [[package]] name = "appdirs" version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = "*" groups = ["dev"] files = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] [[package]] name = "async-timeout" version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" groups = ["main"] files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] [[package]] name = "attrs" version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "click" version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" groups = ["main"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "codespell" version = "2.4.1" description = "Fix common misspellings in text files" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425"}, {file = "codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5"}, ] [package.extras] dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"] hard-encoding-detection = ["chardet"] toml = ["tomli ; python_version < \"3.11\""] types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} [[package]] name = "construct" version = "2.10.70" description = "A powerful declarative symmetric parser/builder for binary data" optional = false python-versions = ">=3.6" groups = ["main"] files = [ {file = "construct-2.10.70-py3-none-any.whl", hash = "sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30"}, {file = "construct-2.10.70.tar.gz", hash = "sha256:4d2472f9684731e58cc9c56c463be63baa1447d674e0d66aeb5627b22f512c29"}, ] [package.extras] extras = ["arrow", "cloudpickle", "cryptography", "lz4", "numpy", "ruamel.yaml"] [[package]] name = "distlib" version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] name = "filelock" version = "3.18.0" description = "A platform independent file lock." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "freezegun" version = "1.5.1" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, ] [package.dependencies] python-dateutil = ">=2.7" [[package]] name = "frozenlist" version = "1.5.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, ] [[package]] name = "identify" version = "2.6.9" description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150"}, {file = "identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "iniconfig" version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] name = "lxml" version = "5.3.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" groups = ["dev"] files = [ {file = "lxml-5.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c4b84d6b580a9625dfa47269bf1fd7fbba7ad69e08b16366a46acb005959c395"}, {file = "lxml-5.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4c08ecb26e4270a62f81f81899dfff91623d349e433b126931c9c4577169666"}, {file = "lxml-5.3.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef926e9f11e307b5a7c97b17c5c609a93fb59ffa8337afac8f89e6fe54eb0b37"}, {file = "lxml-5.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:017ceeabe739100379fe6ed38b033cd244ce2da4e7f6f07903421f57da3a19a2"}, {file = "lxml-5.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dae97d9435dc90590f119d056d233c33006b2fd235dd990d5564992261ee7ae8"}, {file = "lxml-5.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:910f39425c6798ce63c93976ae5af5fff6949e2cb446acbd44d6d892103eaea8"}, {file = "lxml-5.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9780de781a0d62a7c3680d07963db3048b919fc9e3726d9cfd97296a65ffce1"}, {file = "lxml-5.3.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:1a06b0c6ba2e3ca45a009a78a4eb4d6b63831830c0a83dcdc495c13b9ca97d3e"}, {file = "lxml-5.3.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:4c62d0a34d1110769a1bbaf77871a4b711a6f59c4846064ccb78bc9735978644"}, {file = "lxml-5.3.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:8f961a4e82f411b14538fe5efc3e6b953e17f5e809c463f0756a0d0e8039b700"}, {file = "lxml-5.3.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3dfc78f5f9251b6b8ad37c47d4d0bfe63ceb073a916e5b50a3bf5fd67a703335"}, {file = "lxml-5.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10e690bc03214d3537270c88e492b8612d5e41b884f232df2b069b25b09e6711"}, {file = "lxml-5.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa837e6ee9534de8d63bc4c1249e83882a7ac22bd24523f83fad68e6ffdf41ae"}, {file = "lxml-5.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:da4c9223319400b97a2acdfb10926b807e51b69eb7eb80aad4942c0516934858"}, {file = "lxml-5.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc0e9bdb3aa4d1de703a437576007d366b54f52c9897cae1a3716bb44fc1fc85"}, {file = "lxml-5.3.2-cp310-cp310-win32.win32.whl", hash = "sha256:dd755a0a78dd0b2c43f972e7b51a43be518ebc130c9f1a7c4480cf08b4385486"}, {file = "lxml-5.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:d64ea1686474074b38da13ae218d9fde0d1dc6525266976808f41ac98d9d7980"}, {file = "lxml-5.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d61a7d0d208ace43986a92b111e035881c4ed45b1f5b7a270070acae8b0bfb4"}, {file = "lxml-5.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856dfd7eda0b75c29ac80a31a6411ca12209183e866c33faf46e77ace3ce8a79"}, {file = "lxml-5.3.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a01679e4aad0727bedd4c9407d4d65978e920f0200107ceeffd4b019bd48529"}, {file = "lxml-5.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6b37b4c3acb8472d191816d4582379f64d81cecbdce1a668601745c963ca5cc"}, {file = "lxml-5.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3df5a54e7b7c31755383f126d3a84e12a4e0333db4679462ef1165d702517477"}, {file = "lxml-5.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c09a40f28dcded933dc16217d6a092be0cc49ae25811d3b8e937c8060647c353"}, {file = "lxml-5.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1ef20f1851ccfbe6c5a04c67ec1ce49da16ba993fdbabdce87a92926e505412"}, {file = "lxml-5.3.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f79a63289dbaba964eb29ed3c103b7911f2dce28c36fe87c36a114e6bd21d7ad"}, {file = "lxml-5.3.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:75a72697d95f27ae00e75086aed629f117e816387b74a2f2da6ef382b460b710"}, {file = "lxml-5.3.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:b9b00c9ee1cc3a76f1f16e94a23c344e0b6e5c10bec7f94cf2d820ce303b8c01"}, {file = "lxml-5.3.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:77cbcab50cbe8c857c6ba5f37f9a3976499c60eada1bf6d38f88311373d7b4bc"}, {file = "lxml-5.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29424058f072a24622a0a15357bca63d796954758248a72da6d512f9bd9a4493"}, {file = "lxml-5.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7d82737a8afe69a7c80ef31d7626075cc7d6e2267f16bf68af2c764b45ed68ab"}, {file = "lxml-5.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:95473d1d50a5d9fcdb9321fdc0ca6e1edc164dce4c7da13616247d27f3d21e31"}, {file = "lxml-5.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2162068f6da83613f8b2a32ca105e37a564afd0d7009b0b25834d47693ce3538"}, {file = "lxml-5.3.2-cp311-cp311-win32.whl", hash = "sha256:f8695752cf5d639b4e981afe6c99e060621362c416058effd5c704bede9cb5d1"}, {file = "lxml-5.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:d1a94cbb4ee64af3ab386c2d63d6d9e9cf2e256ac0fd30f33ef0a3c88f575174"}, {file = "lxml-5.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:16b3897691ec0316a1aa3c6585f61c8b7978475587c5b16fc1d2c28d283dc1b0"}, {file = "lxml-5.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8d4b34a0eeaf6e73169dcfd653c8d47f25f09d806c010daf074fba2db5e2d3f"}, {file = "lxml-5.3.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cd7a959396da425022e1e4214895b5cfe7de7035a043bcc2d11303792b67554"}, {file = "lxml-5.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cac5eaeec3549c5df7f8f97a5a6db6963b91639389cdd735d5a806370847732b"}, {file = "lxml-5.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b5f7d77334877c2146e7bb8b94e4df980325fab0a8af4d524e5d43cd6f789d"}, {file = "lxml-5.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13f3495cfec24e3d63fffd342cc8141355d1d26ee766ad388775f5c8c5ec3932"}, {file = "lxml-5.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e70ad4c9658beeff99856926fd3ee5fde8b519b92c693f856007177c36eb2e30"}, {file = "lxml-5.3.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:507085365783abd7879fa0a6fa55eddf4bdd06591b17a2418403bb3aff8a267d"}, {file = "lxml-5.3.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:5bb304f67cbf5dfa07edad904732782cbf693286b9cd85af27059c5779131050"}, {file = "lxml-5.3.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:3d84f5c093645c21c29a4e972b84cb7cf682f707f8706484a5a0c7ff13d7a988"}, {file = "lxml-5.3.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bdc13911db524bd63f37b0103af014b7161427ada41f1b0b3c9b5b5a9c1ca927"}, {file = "lxml-5.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ec944539543f66ebc060ae180d47e86aca0188bda9cbfadff47d86b0dc057dc"}, {file = "lxml-5.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:59d437cc8a7f838282df5a199cf26f97ef08f1c0fbec6e84bd6f5cc2b7913f6e"}, {file = "lxml-5.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e275961adbd32e15672e14e0cc976a982075208224ce06d149c92cb43db5b93"}, {file = "lxml-5.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:038aeb6937aa404480c2966b7f26f1440a14005cb0702078c173c028eca72c31"}, {file = "lxml-5.3.2-cp312-cp312-win32.whl", hash = "sha256:3c2c8d0fa3277147bff180e3590be67597e17d365ce94beb2efa3138a2131f71"}, {file = "lxml-5.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:77809fcd97dfda3f399102db1794f7280737b69830cd5c961ac87b3c5c05662d"}, {file = "lxml-5.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:77626571fb5270ceb36134765f25b665b896243529eefe840974269b083e090d"}, {file = "lxml-5.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78a533375dc7aa16d0da44af3cf6e96035e484c8c6b2b2445541a5d4d3d289ee"}, {file = "lxml-5.3.2-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6f62b2404b3f3f0744bbcabb0381c5fe186fa2a9a67ecca3603480f4846c585"}, {file = "lxml-5.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea918da00091194526d40c30c4996971f09dacab032607581f8d8872db34fbf"}, {file = "lxml-5.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c35326f94702a7264aa0eea826a79547d3396a41ae87a70511b9f6e9667ad31c"}, {file = "lxml-5.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3bef90af21d31c4544bc917f51e04f94ae11b43156356aff243cdd84802cbf2"}, {file = "lxml-5.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52fa7ba11a495b7cbce51573c73f638f1dcff7b3ee23697467dc063f75352a69"}, {file = "lxml-5.3.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ad131e2c4d2c3803e736bb69063382334e03648de2a6b8f56a878d700d4b557d"}, {file = "lxml-5.3.2-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:00a4463ca409ceacd20490a893a7e08deec7870840eff33dc3093067b559ce3e"}, {file = "lxml-5.3.2-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:87e8d78205331cace2b73ac8249294c24ae3cba98220687b5b8ec5971a2267f1"}, {file = "lxml-5.3.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bf6389133bb255e530a4f2f553f41c4dd795b1fbb6f797aea1eff308f1e11606"}, {file = "lxml-5.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3709fc752b42fb6b6ffa2ba0a5b9871646d97d011d8f08f4d5b3ee61c7f3b2b"}, {file = "lxml-5.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:abc795703d0de5d83943a4badd770fbe3d1ca16ee4ff3783d7caffc252f309ae"}, {file = "lxml-5.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98050830bb6510159f65d9ad1b8aca27f07c01bb3884ba95f17319ccedc4bcf9"}, {file = "lxml-5.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ba465a91acc419c5682f8b06bcc84a424a7aa5c91c220241c6fd31de2a72bc6"}, {file = "lxml-5.3.2-cp313-cp313-win32.whl", hash = "sha256:56a1d56d60ea1ec940f949d7a309e0bff05243f9bd337f585721605670abb1c1"}, {file = "lxml-5.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:1a580dc232c33d2ad87d02c8a3069d47abbcdce974b9c9cc82a79ff603065dbe"}, {file = "lxml-5.3.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1a59f7fe888d0ec1916d0ad69364c5400cfa2f885ae0576d909f342e94d26bc9"}, {file = "lxml-5.3.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d67b50abc2df68502a26ed2ccea60c1a7054c289fb7fc31c12e5e55e4eec66bd"}, {file = "lxml-5.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cb08d2cb047c98d6fbbb2e77d6edd132ad6e3fa5aa826ffa9ea0c9b1bc74a84"}, {file = "lxml-5.3.2-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:495ddb7e10911fb4d673d8aa8edd98d1eadafb3b56e8c1b5f427fd33cadc455b"}, {file = "lxml-5.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:884d9308ac7d581b705a3371185282e1b8eebefd68ccf288e00a2d47f077cc51"}, {file = "lxml-5.3.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:37f3d7cf7f2dd2520df6cc8a13df4c3e3f913c8e0a1f9a875e44f9e5f98d7fee"}, {file = "lxml-5.3.2-cp36-cp36m-win32.whl", hash = "sha256:e885a1bf98a76dff0a0648850c3083b99d9358ef91ba8fa307c681e8e0732503"}, {file = "lxml-5.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b45f505d0d85f4cdd440cd7500689b8e95110371eaa09da0c0b1103e9a05030f"}, {file = "lxml-5.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b53cd668facd60b4f0dfcf092e01bbfefd88271b5b4e7b08eca3184dd006cb30"}, {file = "lxml-5.3.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5dea998c891f082fe204dec6565dbc2f9304478f2fc97bd4d7a940fec16c873"}, {file = "lxml-5.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d46bc3e58b01e4f38d75e0d7f745a46875b7a282df145aca9d1479c65ff11561"}, {file = "lxml-5.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661feadde89159fd5f7d7639a81ccae36eec46974c4a4d5ccce533e2488949c8"}, {file = "lxml-5.3.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:43af2a69af2cacc2039024da08a90174e85f3af53483e6b2e3485ced1bf37151"}, {file = "lxml-5.3.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:1539f962d82436f3d386eb9f29b2a29bb42b80199c74a695dff51b367a61ec0a"}, {file = "lxml-5.3.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:6673920bf976421b5fac4f29b937702eef4555ee42329546a5fc68bae6178a48"}, {file = "lxml-5.3.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9fa722a9cd8845594593cce399a49aa6bfc13b6c83a7ee05e2ab346d9253d52f"}, {file = "lxml-5.3.2-cp37-cp37m-win32.whl", hash = "sha256:2eadd4efa487f4710755415aed3d6ae9ac8b4327ea45226ffccb239766c8c610"}, {file = "lxml-5.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:83d8707b1b08cd02c04d3056230ec3b771b18c566ec35e723e60cdf037064e08"}, {file = "lxml-5.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc6e8678bfa5ccba370103976ccfcf776c85c83da9220ead41ea6fd15d2277b4"}, {file = "lxml-5.3.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bed509662f67f719119ad56006cd4a38efa68cfa74383060612044915e5f7ad"}, {file = "lxml-5.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3925975fadd6fd72a6d80541a6ec75dfbad54044a03aa37282dafcb80fbdfa"}, {file = "lxml-5.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83c0462dedc5213ac586164c6d7227da9d4d578cf45dd7fbab2ac49b63a008eb"}, {file = "lxml-5.3.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:53e3f9ca72858834688afa17278649d62aa768a4b2018344be00c399c4d29e95"}, {file = "lxml-5.3.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:32ba634ef3f1b20f781019a91d78599224dc45745dd572f951adbf1c0c9b0d75"}, {file = "lxml-5.3.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1b16504c53f41da5fcf04868a80ac40a39d3eec5329caf761114caec6e844ad1"}, {file = "lxml-5.3.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:1f9682786138549da44ca4c49b20e7144d063b75f2b2ba611f4cff9b83db1062"}, {file = "lxml-5.3.2-cp38-cp38-win32.whl", hash = "sha256:d8f74ef8aacdf6ee5c07566a597634bb8535f6b53dc89790db43412498cf6026"}, {file = "lxml-5.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:49f1cee0fa27e1ee02589c696a9bdf4027e7427f184fa98e6bef0c6613f6f0fa"}, {file = "lxml-5.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:741c126bcf9aa939e950e64e5e0a89c8e01eda7a5f5ffdfc67073f2ed849caea"}, {file = "lxml-5.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ab6e9e6aca1fd7d725ffa132286e70dee5b9a4561c5ed291e836440b82888f89"}, {file = "lxml-5.3.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58e8c9b9ed3c15c2d96943c14efc324b69be6352fe5585733a7db2bf94d97841"}, {file = "lxml-5.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7811828ddfb8c23f4f1fbf35e7a7b2edec2f2e4c793dee7c52014f28c4b35238"}, {file = "lxml-5.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72968623efb1e12e950cbdcd1d0f28eb14c8535bf4be153f1bfffa818b1cf189"}, {file = "lxml-5.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebfceaa2ea588b54efb6160e3520983663d45aed8a3895bb2031ada080fb5f04"}, {file = "lxml-5.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d685d458505b2bfd2e28c812749fe9194a2b0ce285a83537e4309a187ffa270b"}, {file = "lxml-5.3.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:334e0e414dab1f5366ead8ca34ec3148415f236d5660e175f1d640b11d645847"}, {file = "lxml-5.3.2-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02e56f7de72fa82561eae69628a7d6febd7891d72248c7ff7d3e7814d4031017"}, {file = "lxml-5.3.2-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:638d06b4e1d34d1a074fa87deed5fb55c18485fa0dab97abc5604aad84c12031"}, {file = "lxml-5.3.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:354dab7206d22d7a796fa27c4c5bffddd2393da2ad61835355a4759d435beb47"}, {file = "lxml-5.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d9d9f82ff2c3bf9bb777cb355149f7f3a98ec58f16b7428369dc27ea89556a4c"}, {file = "lxml-5.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:95ad58340e3b7d2b828efc370d1791856613c5cb62ae267158d96e47b3c978c9"}, {file = "lxml-5.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:30fe05f4b7f6e9eb32862745512e7cbd021070ad0f289a7f48d14a0d3fc1d8a9"}, {file = "lxml-5.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34c688fef86f73dbca0798e0a61bada114677006afa524a8ce97d9e5fabf42e6"}, {file = "lxml-5.3.2-cp39-cp39-win32.whl", hash = "sha256:4d6d3d1436d57f41984920667ec5ef04bcb158f80df89ac4d0d3f775a2ac0c87"}, {file = "lxml-5.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:2996e1116bbb3ae2a1fbb2ba4da8f92742290b4011e7e5bce2bd33bbc9d9485a"}, {file = "lxml-5.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:521ab9c80b98c30b2d987001c3ede2e647e92eeb2ca02e8cb66ef5122d792b24"}, {file = "lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1231b0f9810289d41df1eacc4ebb859c63e4ceee29908a0217403cddce38d0"}, {file = "lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271f1a4d5d2b383c36ad8b9b489da5ea9c04eca795a215bae61ed6a57cf083cd"}, {file = "lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6fca8a5a13906ba2677a5252752832beb0f483a22f6c86c71a2bb320fba04f61"}, {file = "lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ea0c3b7922209160faef194a5b6995bfe7fa05ff7dda6c423ba17646b7b9de10"}, {file = "lxml-5.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0a006390834603e5952a2ff74b9a31a6007c7cc74282a087aa6467afb4eea987"}, {file = "lxml-5.3.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:eae4136a3b8c4cf76f69461fc8f9410d55d34ea48e1185338848a888d71b9675"}, {file = "lxml-5.3.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d48e06be8d8c58e7feaedd8a37897a6122637efb1637d7ce00ddf5f11f9a92ad"}, {file = "lxml-5.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4b83aed409134093d90e114007034d2c1ebcd92e501b71fd9ec70e612c8b2eb"}, {file = "lxml-5.3.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7a0e77edfe26d3703f954d46bed52c3ec55f58586f18f4b7f581fc56954f1d84"}, {file = "lxml-5.3.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:19f6fcfd15b82036b4d235749d78785eb9c991c7812012dc084e0d8853b4c1c0"}, {file = "lxml-5.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d49919c95d31ee06eefd43d8c6f69a3cc9bdf0a9b979cc234c4071f0eb5cb173"}, {file = "lxml-5.3.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2d0a60841410123c533990f392819804a8448853f06daf412c0f383443925e89"}, {file = "lxml-5.3.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7f729e03090eb4e3981f10efaee35e6004b548636b1a062b8b9a525e752abc"}, {file = "lxml-5.3.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:579df6e20d8acce3bcbc9fb8389e6ae00c19562e929753f534ba4c29cfe0be4b"}, {file = "lxml-5.3.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2abcf3f3b8367d6400b908d00d4cd279fc0b8efa287e9043820525762d383699"}, {file = "lxml-5.3.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:348c06cb2e3176ce98bee8c397ecc89181681afd13d85870df46167f140a305f"}, {file = "lxml-5.3.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:617ecaccd565cbf1ac82ffcaa410e7da5bd3a4b892bb3543fb2fe19bd1c4467d"}, {file = "lxml-5.3.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c3eb4278dcdb9d86265ed2c20b9ecac45f2d6072e3904542e591e382c87a9c00"}, {file = "lxml-5.3.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258b6b53458c5cbd2a88795557ff7e0db99f73a96601b70bc039114cd4ee9e02"}, {file = "lxml-5.3.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a9d8d25ed2f2183e8471c97d512a31153e123ac5807f61396158ef2793cb6e"}, {file = "lxml-5.3.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73bcb635a848c18a3e422ea0ab0092f2e4ef3b02d8ebe87ab49748ebc8ec03d8"}, {file = "lxml-5.3.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1545de0a69a16ced5767bae8cca1801b842e6e49e96f5e4a8a5acbef023d970b"}, {file = "lxml-5.3.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:165fcdc2f40fc0fe88a3c3c06c9c2a097388a90bda6a16e6f7c9199c903c9b8e"}, {file = "lxml-5.3.2.tar.gz", hash = "sha256:773947d0ed809ddad824b7b14467e1a481b8976e87278ac4a730c2f7c7fcddc1"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=3.0.11,<3.1.0)"] [[package]] name = "multidict" version = "6.3.2" description = "multidict implementation" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "multidict-6.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8b3dc0eec9304fa04d84a51ea13b0ec170bace5b7ddeaac748149efd316f1504"}, {file = "multidict-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9534f3d84addd3b6018fa83f97c9d4247aaa94ac917d1ed7b2523306f99f5c16"}, {file = "multidict-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a003ce1413ae01f0b8789c1c987991346a94620a4d22210f7a8fe753646d3209"}, {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b43f7384e68b1b982c99f489921a459467b5584bdb963b25e0df57c9039d0ad"}, {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d142ae84047262dc75c1f92eaf95b20680f85ce11d35571b4c97e267f96fadc4"}, {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec7e86fbc48aa1d6d686501a8547818ba8d645e7e40eaa98232a5d43ee4380ad"}, {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe019fb437632b016e6cac67a7e964f1ef827ef4023f1ca0227b54be354da97e"}, {file = "multidict-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b60cb81214a9da7cfd8ae2853d5e6e47225ece55fe5833142fe0af321c35299"}, {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32d9e8ef2e0312d4e96ca9adc88e0675b6d8e144349efce4a7c95d5ccb6d88e0"}, {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:335d584312e3fa43633d63175dfc1a5f137dd7aa03d38d1310237d54c3032774"}, {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8df917faa6b8cac3d6870fc21cb7e4d169faca68e43ffe568c156c9c6408a4d"}, {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc060b9b89b701dd8fedef5b99e1f1002b8cb95072693233a63389d37e48212d"}, {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f2ce3be2500658f3c644494b934628bb0c82e549dde250d2119689ce791cc8b8"}, {file = "multidict-6.3.2-cp310-cp310-win32.whl", hash = "sha256:dbcb4490d8e74b484449abd51751b8f560dd0a4812eb5dacc6a588498222a9ab"}, {file = "multidict-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:06944f9ced30f8602be873563ed4df7e3f40958f60b2db39732c11d615a33687"}, {file = "multidict-6.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45a034f41fcd16968c0470d8912d293d7b0d0822fc25739c5c2ff7835b85bc56"}, {file = "multidict-6.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:352585cec45f5d83d886fc522955492bb436fca032b11d487b12d31c5a81b9e3"}, {file = "multidict-6.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da9d89d293511fd0a83a90559dc131f8b3292b6975eb80feff19e5f4663647e2"}, {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fa716592224aa652b9347a586cfe018635229074565663894eb4eb21f8307f"}, {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0326278a44c56e94792475268e5cd3d47fbc0bd41ee56928c3bbb103ba7f58fe"}, {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb1ea87f7fe45e5079f6315e95d64d4ca8b43ef656d98bed63a02e3756853a22"}, {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cff3c5a98d037024a9065aafc621a8599fad7b423393685dc83cf7a32f8b691"}, {file = "multidict-6.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed99834b053c655d980fb98029003cb24281e47a796052faad4543aa9e01b8e8"}, {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7048440e505d2b4741e5d0b32bd2f427c901f38c7760fc245918be2cf69b3b85"}, {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27248c27b563f5889556da8a96e18e98a56ff807ac1a7d56cf4453c2c9e4cd91"}, {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6323b4ba0e018bd266f776c35f3f0943fc4ee77e481593c9f93bd49888f24e94"}, {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:81f7ce5ec7c27d0b45c10449c8f0fed192b93251e2e98cb0b21fec779ef1dc4d"}, {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03bfcf2825b3bed0ba08a9d854acd18b938cab0d2dba3372b51c78e496bac811"}, {file = "multidict-6.3.2-cp311-cp311-win32.whl", hash = "sha256:f32c2790512cae6ca886920e58cdc8c784bdc4bb2a5ec74127c71980369d18dc"}, {file = "multidict-6.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b0c15e58e038a2cd75ef7cf7e072bc39b5e0488b165902efb27978984bbad70"}, {file = "multidict-6.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d1e0ba1ce1b8cc79117196642d95f4365e118eaf5fb85f57cdbcc5a25640b2a4"}, {file = "multidict-6.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:029bbd7d782251a78975214b78ee632672310f9233d49531fc93e8e99154af25"}, {file = "multidict-6.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7db41e3b56817d9175264e5fe00192fbcb8e1265307a59f53dede86161b150e"}, {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcab18e65cc555ac29981a581518c23311f2b1e72d8f658f9891590465383be"}, {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d50eff89aa4d145a5486b171a2177042d08ea5105f813027eb1050abe91839f"}, {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:643e57b403d3e240045a3681f9e6a04d35a33eddc501b4cbbbdbc9c70122e7bc"}, {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d17b37b9715b30605b5bab1460569742d0c309e5c20079263b440f5d7746e7e"}, {file = "multidict-6.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68acd51fa94e63312b8ddf84bfc9c3d3442fe1f9988bbe1b6c703043af8867fe"}, {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:347eea2852ab7f697cc5ed9b1aae96b08f8529cca0c6468f747f0781b1842898"}, {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4d3f8e57027dcda84a1aa181501c15c45eab9566eb6fcc274cbd1e7561224f8"}, {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9ca57a841ffcf712e47875d026aa49d6e67f9560624d54b51628603700d5d287"}, {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7cafdafb44c4e646118410368307693e49d19167e5f119cbe3a88697d2d1a636"}, {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:430120c6ce3715a9c6075cabcee557daccbcca8ba25a9fedf05c7bf564532f2d"}, {file = "multidict-6.3.2-cp312-cp312-win32.whl", hash = "sha256:13bec31375235a68457ab887ce1bbf4f59d5810d838ae5d7e5b416242e1f3ed4"}, {file = "multidict-6.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:c3b6d7620e6e90c6d97eaf3a63bf7fbd2ba253aab89120a4a9c660bf2d675391"}, {file = "multidict-6.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b9ca24700322816ae0d426aa33671cf68242f8cc85cee0d0e936465ddaee90b5"}, {file = "multidict-6.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d9fbbe23667d596ff4f9f74d44b06e40ebb0ab6b262cf14a284f859a66f86457"}, {file = "multidict-6.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cb602c5bea0589570ad3a4a6f2649c4f13cc7a1e97b4c616e5e9ff8dc490987"}, {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93ca81dd4d1542e20000ed90f4cc84b7713776f620d04c2b75b8efbe61106c99"}, {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18b6310b5454c62242577a128c87df8897f39dd913311cf2e1298e47dfc089eb"}, {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a6dda57de1fc9aedfdb600a8640c99385cdab59a5716cb714b52b6005797f77"}, {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d8ec42d03cc6b29845552a68151f9e623c541f1708328353220af571e24a247"}, {file = "multidict-6.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80681969cee2fa84dafeb53615d51d24246849984e3e87fbe4fe39956f2e23bf"}, {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:01489b0c3592bb9d238e5690e9566db7f77a5380f054b57077d2c4deeaade0eb"}, {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:522d9f1fd995d04dfedc0a40bca7e2591bc577d920079df50b56245a4a252c1c"}, {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2014e9cf0b4e9c75bbad49c1758e5a9bf967a56184fc5fcc51527425baf5abba"}, {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:78ced9fcbee79e446ff4bb3018ac7ba1670703de7873d9c1f6f9883db53c71bc"}, {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1faf01af972bd01216a107c195f5294f9f393531bc3e4faddc9b333581255d4d"}, {file = "multidict-6.3.2-cp313-cp313-win32.whl", hash = "sha256:7a699ab13d8d8e1f885de1535b4f477fb93836c87168318244c2685da7b7f655"}, {file = "multidict-6.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:8666bb0d883310c83be01676e302587834dfd185b52758caeab32ef0eb387bc6"}, {file = "multidict-6.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:d82c95aabee29612b1c4f48b98be98181686eb7d6c0152301f72715705cc787b"}, {file = "multidict-6.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f47709173ea9e87a7fd05cd7e5cf1e5d4158924ff988a9a8e0fbd853705f0e68"}, {file = "multidict-6.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c7f9d0276ceaab41b8ae78534ff28ea33d5de85db551cbf80c44371f2b55d13"}, {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6eab22df44a25acab2e738f882f5ec551282ab45b2bbda5301e6d2cfb323036"}, {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a947cb7c657f57874021b9b70c7aac049c877fb576955a40afa8df71d01a1390"}, {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5faa346e8e1c371187cf345ab1e02a75889f9f510c9cbc575c31b779f7df084d"}, {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc6e08d977aebf1718540533b4ba5b351ccec2db093370958a653b1f7f9219cc"}, {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98eab7acf55275b5bf09834125fa3a80b143a9f241cdcdd3f1295ffdc3c6d097"}, {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:36863655630becc224375c0b99364978a0f95aebfb27fb6dd500f7fb5fb36e79"}, {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d9c0979c096c0d46a963331b0e400d3a9e560e41219df4b35f0d7a2f28f39710"}, {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0efc04f70f05e70e5945890767e8874da5953a196f5b07c552d305afae0f3bf6"}, {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:2c519b3b82c34539fae3e22e4ea965869ac6b628794b1eb487780dde37637ab7"}, {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:329160e301f2afd7b43725d3dda8a7ef8ee41d4ceac2083fc0d8c1cc8a4bd56b"}, {file = "multidict-6.3.2-cp313-cp313t-win32.whl", hash = "sha256:420e5144a5f598dad8db3128f1695cd42a38a0026c2991091dab91697832f8cc"}, {file = "multidict-6.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:875faded2861c7af2682c67088e6313fec35ede811e071c96d36b081873cea14"}, {file = "multidict-6.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2516c5eb5732d6c4e29fa93323bfdc55186895124bc569e2404e3820934be378"}, {file = "multidict-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:be5c8622e665cc5491c13c0fcd52915cdbae991a3514251d71129691338cdfb2"}, {file = "multidict-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ef33150eea7953cfdb571d862cff894e0ad97ab80d97731eb4b9328fc32d52b"}, {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b357738ce46e998f1b1bad9c4b79b2a9755915f71b87a8c01ce123a22a4f99"}, {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c60e059fcd3655a653ba99fec2556cd0260ec57f9cb138d3e6ffc413638a2e"}, {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:629e7c5e75bde83e54a22c7043ce89d68691d1f103be6d09a1c82b870df3b4b8"}, {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6c8fc97d893fdf1fff15a619fee8de2f31c9b289ef7594730e35074fa0cefb"}, {file = "multidict-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52081d2f27e0652265d4637b03f09b82f6da5ce5e1474f07dc64674ff8bfc04c"}, {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:64529dc395b5fd0a7826ffa70d2d9a7f4abd8f5333d6aaaba67fdf7bedde9f21"}, {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2b7c3fad827770840f5399348c89635ed6d6e9bba363baad7d3c7f86a9cf1da3"}, {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:24aa42b1651c654ae9e5273e06c3b7ccffe9f7cc76fbde40c37e9ae65f170818"}, {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:04ceea01e9991357164b12882e120ce6b4d63a0424bb9f9cd37910aa56d30830"}, {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:943897a41160945416617db567d867ab34e9258adaffc56a25a4c3f99d919598"}, {file = "multidict-6.3.2-cp39-cp39-win32.whl", hash = "sha256:76157a9a0c5380aadd3b5ff7b8deee355ff5adecc66c837b444fa633b4d409a2"}, {file = "multidict-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:d091d123e44035cd5664554308477aff0b58db37e701e7598a67e907b98d1925"}, {file = "multidict-6.3.2-py3-none-any.whl", hash = "sha256:71409d4579f716217f23be2f5e7afca5ca926aaeb398aa11b72d793bff637a1f"}, {file = "multidict-6.3.2.tar.gz", hash = "sha256:c1035eea471f759fa853dd6e76aaa1e389f93b3e1403093fa0fd3ab4db490678"}, ] [[package]] name = "mypy" version = "1.15.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, ] [package.dependencies] mypy_extensions = ">=1.0.0" typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] faster-cache = ["orjson"] 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." optional = false python-versions = ">=3.5" groups = ["dev"] 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 = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] [[package]] name = "packaging" version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "paho-mqtt" version = "2.1.0" description = "MQTT version 5.0/3.1.1 client class" optional = false python-versions = ">=3.7" groups = ["main"] files = [ {file = "paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee"}, {file = "paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834"}, ] [package.extras] proxy = ["pysocks"] [[package]] name = "pillow" version = "11.1.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, ] [package.extras] docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] name = "platformdirs" version = "4.3.7" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, ] [package.extras] docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" version = "4.2.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "propcache" version = "0.3.1" description = "Accelerated property cache" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, {file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"}, {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"}, {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"}, {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"}, {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"}, {file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"}, {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"}, {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"}, {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"}, {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"}, {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"}, {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"}, {file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"}, {file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"}, {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"}, {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"}, {file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"}, {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"}, {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"}, {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"}, {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"}, {file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"}, {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"}, {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"}, {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"}, {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"}, {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"}, {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"}, {file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"}, {file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"}, {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"}, {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"}, {file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"}, {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"}, {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"}, {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"}, {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"}, {file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"}, {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"}, {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"}, {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"}, {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"}, {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"}, {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"}, {file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"}, {file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"}, {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"}, {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"}, {file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"}, {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"}, {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"}, {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"}, {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"}, {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"}, {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"}, {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"}, {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"}, {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"}, {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"}, {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"}, {file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"}, {file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"}, {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"}, {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"}, {file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"}, {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"}, {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"}, {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"}, {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"}, {file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"}, {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"}, {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"}, {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"}, {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"}, {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"}, {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"}, {file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"}, {file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"}, {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, ] [[package]] name = "pycryptodome" version = "3.22.0" description = "Cryptographic library for Python" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main"] files = [ {file = "pycryptodome-3.22.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:96e73527c9185a3d9b4c6d1cfb4494f6ced418573150be170f6580cb975a7f5a"}, {file = "pycryptodome-3.22.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:9e1bb165ea1dc83a11e5dbbe00ef2c378d148f3a2d3834fb5ba4e0f6fd0afe4b"}, {file = "pycryptodome-3.22.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:d4d1174677855c266eed5c4b4e25daa4225ad0c9ffe7584bb1816767892545d0"}, {file = "pycryptodome-3.22.0-cp27-cp27m-win32.whl", hash = "sha256:9dbb749cef71c28271484cbef684f9b5b19962153487735411e1020ca3f59cb1"}, {file = "pycryptodome-3.22.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f1ae7beb64d4fc4903a6a6cca80f1f448e7a8a95b77d106f8a29f2eb44d17547"}, {file = "pycryptodome-3.22.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a26bcfee1293b7257c83b0bd13235a4ee58165352be4f8c45db851ba46996dc6"}, {file = "pycryptodome-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:009e1c80eea42401a5bd5983c4bab8d516aef22e014a4705622e24e6d9d703c6"}, {file = "pycryptodome-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b76fa80daeff9519d7e9f6d9e40708f2fce36b9295a847f00624a08293f4f00"}, {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a31fa5914b255ab62aac9265654292ce0404f6b66540a065f538466474baedbc"}, {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0092fd476701eeeb04df5cc509d8b739fa381583cda6a46ff0a60639b7cd70d"}, {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d5b0ddc7cf69231736d778bd3ae2b3efb681ae33b64b0c92fb4626bb48bb89"}, {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f6cf6aa36fcf463e622d2165a5ad9963b2762bebae2f632d719dfb8544903cf5"}, {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:aec7b40a7ea5af7c40f8837adf20a137d5e11a6eb202cde7e588a48fb2d871a8"}, {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772"}, {file = "pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb"}, {file = "pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627"}, {file = "pycryptodome-3.22.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:98fd9da809d5675f3a65dcd9ed384b9dc67edab6a4cda150c5870a8122ec961d"}, {file = "pycryptodome-3.22.0-pp27-pypy_73-win32.whl", hash = "sha256:37ddcd18284e6b36b0a71ea495a4c4dca35bb09ccc9bfd5b91bfaf2321f131c1"}, {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4bdce34af16c1dcc7f8c66185684be15f5818afd2a82b75a4ce6b55f9783e13"}, {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2988ffcd5137dc2d27eb51cd18c0f0f68e5b009d5fec56fbccb638f90934f333"}, {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e653519dedcd1532788547f00eeb6108cc7ce9efdf5cc9996abce0d53f95d5a9"}, {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5810bc7494e4ac12a4afef5a32218129e7d3890ce3f2b5ec520cc69eb1102ad"}, {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7514a1aebee8e85802d154fdb261381f1cb9b7c5a54594545145b8ec3056ae6"}, {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:56c6f9342fcb6c74e205fbd2fee568ec4cdbdaa6165c8fde55dbc4ba5f584464"}, {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87a88dc543b62b5c669895caf6c5a958ac7abc8863919e94b7a6cafd2f64064f"}, {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a683bc9fa585c0dfec7fa4801c96a48d30b30b096e3297f9374f40c2fedafc"}, {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f4f6f47a7f411f2c157e77bbbda289e0c9f9e1e9944caa73c1c2e33f3f92d6e"}, {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6cf9553b29624961cab0785a3177a333e09e37ba62ad22314ebdbb01ca79840"}, {file = "pycryptodome-3.22.0.tar.gz", hash = "sha256:fd7ab568b3ad7b77c908d7c3f7e167ec5a8f035c64ff74f10d47a4edd043d723"}, ] [[package]] name = "pycryptodomex" version = "3.22.0" description = "Cryptographic library for Python" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main"] markers = "sys_platform == \"darwin\"" files = [ {file = "pycryptodomex-3.22.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:41673e5cc39a8524557a0472077635d981172182c9fe39ce0b5f5c19381ffaff"}, {file = "pycryptodomex-3.22.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:276be1ed006e8fd01bba00d9bd9b60a0151e478033e86ea1cb37447bbc057edc"}, {file = "pycryptodomex-3.22.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:813e57da5ceb4b549bab96fa548781d9a63f49f1d68fdb148eeac846238056b7"}, {file = "pycryptodomex-3.22.0-cp27-cp27m-win32.whl", hash = "sha256:d7beeacb5394765aa8dabed135389a11ee322d3ee16160d178adc7f8ee3e1f65"}, {file = "pycryptodomex-3.22.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:b3746dedf74787da43e4a2f85bd78f5ec14d2469eb299ddce22518b3891f16ea"}, {file = "pycryptodomex-3.22.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5ebc09b7d8964654aaf8a4f5ac325f2b0cc038af9bea12efff0cd4a5bb19aa42"}, {file = "pycryptodomex-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:aef4590263b9f2f6283469e998574d0bd45c14fb262241c27055b82727426157"}, {file = "pycryptodomex-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5ac608a6dce9418d4f300fab7ba2f7d499a96b462f2b9b5c90d8d994cd36dcad"}, {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a24f681365ec9757ccd69b85868bbd7216ba451d0f86f6ea0eed75eeb6975db"}, {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:259664c4803a1fa260d5afb322972813c5fe30ea8b43e54b03b7e3a27b30856b"}, {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7127d9de3c7ce20339e06bcd4f16f1a1a77f1471bcf04e3b704306dde101b719"}, {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee75067b35c93cc18b38af47b7c0664998d8815174cfc66dd00ea1e244eb27e6"}, {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:1a8b0c5ba061ace4bcd03496d42702c3927003db805b8ec619ea6506080b381d"}, {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bfe4fe3233ef3e58028a3ad8f28473653b78c6d56e088ea04fe7550c63d4d16b"}, {file = "pycryptodomex-3.22.0-cp37-abi3-win32.whl", hash = "sha256:2cac9ed5c343bb3d0075db6e797e6112514764d08d667c74cb89b931aac9dddd"}, {file = "pycryptodomex-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:ff46212fda7ee86ec2f4a64016c994e8ad80f11ef748131753adb67e9b722ebd"}, {file = "pycryptodomex-3.22.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:5bf3ce9211d2a9877b00b8e524593e2209e370a287b3d5e61a8c45f5198487e2"}, {file = "pycryptodomex-3.22.0-pp27-pypy_73-win32.whl", hash = "sha256:684cb57812cd243217c3d1e01a720c5844b30f0b7b64bb1a49679f7e1e8a54ac"}, {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c8cffb03f5dee1026e3f892f7cffd79926a538c67c34f8b07c90c0bd5c834e27"}, {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:140b27caa68a36d0501b05eb247bd33afa5f854c1ee04140e38af63c750d4e39"}, {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:644834b1836bb8e1d304afaf794d5ae98a1d637bd6e140c9be7dd192b5374811"}, {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c506aba3318505dbeecf821ed7b9a9f86f422ed085e2d79c4fba0ae669920a"}, {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7cd39f7a110c1ab97ce9ee3459b8bc615920344dc00e56d1b709628965fba3f2"}, {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e4eaaf6163ff13788c1f8f615ad60cdc69efac6d3bf7b310b21e8cfe5f46c801"}, {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eac39e237d65981554c2d4c6668192dc7051ad61ab5fc383ed0ba049e4007ca2"}, {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ab0d89d1761959b608952c7b347b0e76a32d1a5bb278afbaa10a7f3eaef9a0a"}, {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e64164f816f5e43fd69f8ed98eb28f98157faf68208cd19c44ed9d8e72d33e8"}, {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f005de31efad6f9acefc417296c641f13b720be7dbfec90edeaca601c0fab048"}, {file = "pycryptodomex-3.22.0.tar.gz", hash = "sha256:a1da61bacc22f93a91cbe690e3eb2022a03ab4123690ab16c46abb693a9df63d"}, ] [[package]] name = "pyrate-limiter" version = "3.7.0" description = "Python Rate-Limiter using Leaky-Bucket Algorithm" optional = false python-versions = "<4.0,>=3.8" groups = ["main"] files = [ {file = "pyrate_limiter-3.7.0-py3-none-any.whl", hash = "sha256:cdbfc8f537d07e2bda76f5191b38aee972b26e1af020d880e3c1ef9d528227ac"}, {file = "pyrate_limiter-3.7.0.tar.gz", hash = "sha256:dc1e6d2c80b559f3333cb44bd822bd558f5a47946dc50cce4263a9c1c5fd8067"}, ] [package.extras] all = ["filelock (>=3.0)", "psycopg[pool] (>=3.1.18,<4.0.0)", "redis (>=5.0.0,<6.0.0)"] docs = ["furo (>=2022.3.4,<2023.0.0)", "myst-parser (>=0.17)", "sphinx (>=4.3.0,<5.0.0)", "sphinx-autodoc-typehints (>=1.17,<2.0)", "sphinx-copybutton (>=0.5)", "sphinxcontrib-apidoc (>=0.3,<0.4)"] [[package]] name = "pyshark" version = "0.6" description = "Python wrapper for tshark, allowing python packet parsing using wireshark dissectors" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "pyshark-0.6-py3-none-any.whl", hash = "sha256:98e8a1ebdcbfbb6e8defd0c96736ea51bf8234339f980b15dd3545f87f5146d4"}, {file = "pyshark-0.6.tar.gz", hash = "sha256:a424d83e0ca6224a96bbe30cd3f89d5491654d783faaaf90adaf45867a0bcb17"}, ] [package.dependencies] appdirs = "*" lxml = "*" packaging = "*" termcolor = "*" [[package]] name = "pytest" version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "0.26.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"}, {file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"}, ] [package.dependencies] pytest = ">=8.2,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-timeout" version = "2.3.1" description = "pytest plugin to abort hanging tests" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, ] [package.dependencies] pytest = ">=7.0.0" [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["dev"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] six = ">=1.5" [[package]] name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] name = "ruff" version = "0.11.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2"}, {file = "ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc"}, {file = "ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906"}, {file = "ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f"}, {file = "ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e"}, {file = "ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223"}, {file = "ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e"}, {file = "ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d"}, {file = "ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99"}, {file = "ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222"}, {file = "ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304"}, {file = "ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019"}, {file = "ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896"}, {file = "ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751"}, {file = "ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270"}, {file = "ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb"}, {file = "ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc"}, {file = "ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407"}, ] [[package]] name = "six" version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "termcolor" version = "3.0.1" description = "ANSI color formatting for output in terminal" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "termcolor-3.0.1-py3-none-any.whl", hash = "sha256:da1ed4ec8a5dc5b2e17476d859febdb3cccb612be1c36e64511a6f2485c10c69"}, {file = "termcolor-3.0.1.tar.gz", hash = "sha256:a6abd5c6e1284cea2934443ba806e70e5ec8fd2449021be55c280f8a3731b611"}, ] [package.extras] tests = ["pytest", "pytest-cov"] [[package]] name = "typing-extensions" version = "4.13.1" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69"}, {file = "typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff"}, ] [[package]] name = "vacuum-map-parser-base" version = "0.1.3" description = "Common code for vacuum map parsers" optional = false python-versions = "<4.0,>=3.11" groups = ["main"] files = [ {file = "vacuum_map_parser_base-0.1.3-py3-none-any.whl", hash = "sha256:986b51f0b6492cdff4df6a01816a1d14508273015a8a146dbb8cfc814b3abf4e"}, {file = "vacuum_map_parser_base-0.1.3.tar.gz", hash = "sha256:e7271d426dc7f6c29f039965d6d2b8167537bee327a131d28be3c5da89b3d24f"}, ] [package.dependencies] Pillow = "*" [[package]] name = "vacuum-map-parser-roborock" version = "0.1.2" description = "Functionalities for Roborock vacuum map parsing" optional = false python-versions = "<4.0,>=3.11" groups = ["main"] files = [ {file = "vacuum_map_parser_roborock-0.1.2-py3-none-any.whl", hash = "sha256:7b66bc3af556db46f8c126793d24307d0e9117307ee1b952da1c86148c79c369"}, {file = "vacuum_map_parser_roborock-0.1.2.tar.gz", hash = "sha256:e910b8a0349be8224fe25ca27b9ea088b66f971c30715621b3e187868bd028d0"}, ] [package.dependencies] Pillow = "*" vacuum-map-parser-base = "0.1.3" [[package]] name = "virtualenv" version = "20.30.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "yarl" version = "1.18.3" description = "Yet another URL library" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = "^3.11" content-hash = "539acd1831188994429cea11425e53ec1b851f17f7f7e92ed466865190fa80c4" python-roborock-2.19.0/pyproject.toml000066400000000000000000000036151501065527500176710ustar00rootroot00000000000000[tool.poetry] name = "python-roborock" version = "2.19.0" description = "A package to control Roborock vacuums." authors = ["humbertogontijo "] license = "GPL-3.0-only" readme = "README.md" repository = "https://github.com/humbertogontijo/python-roborock" documentation = "https://python-roborock.readthedocs.io/" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries", ] packages = [{include = "roborock"}] keywords = ["roborock", "vacuum", "homeassistant"] [tool.poetry.scripts] roborock = "roborock.cli:main" [tool.poetry.dependencies] python = "^3.11" click = ">=8" aiohttp = "^3.8.2" async-timeout = "*" pycryptodome = "^3.18" pycryptodomex = {version = "^3.18", markers = "sys_platform == 'darwin'"} paho-mqtt = ">=1.6.1,<3.0.0" construct = "^2.10.57" vacuum-map-parser-roborock = "*" pyrate-limiter = "^3.7.0" aiomqtt = "^2.3.2" [build-system] requires = ["poetry-core==1.8.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.group.dev.dependencies] pytest-asyncio = "*" pytest = "*" pre-commit = ">=3.5,<5.0" mypy = "*" ruff = "*" codespell = "*" pyshark = "^0.6" aioresponses = "^0.7.7" freezegun = "^1.5.1" pytest-timeout = "^2.3.1" [tool.semantic_release] branch = "main" version_toml = ["pyproject.toml:tool.poetry.version"] build_command = "pip install poetry && poetry build" [tool.semantic_release.commit_parser_options] allowed_tags = [ "chore", "docs", "feat", "fix", "refactor" ] major_tags= ["refactor"] [tool.ruff] ignore = ["F403", "E741"] line-length = 120 select=["E", "F", "UP", "I"] [tool.ruff.lint.per-file-ignores] "*/__init__.py" = ["F401"] [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" timeout = 20 python-roborock-2.19.0/roborock/000077500000000000000000000000001501065527500165705ustar00rootroot00000000000000python-roborock-2.19.0/roborock/__init__.py000066400000000000000000000002451501065527500207020ustar00rootroot00000000000000"""Roborock API.""" from roborock.code_mappings import * from roborock.containers import * from roborock.exceptions import * from roborock.roborock_typing import * python-roborock-2.19.0/roborock/api.py000066400000000000000000000100141501065527500177070ustar00rootroot00000000000000"""The Roborock api.""" from __future__ import annotations import asyncio import base64 import logging import secrets import time from abc import ABC, abstractmethod from typing import Any from .containers import ( DeviceData, ) from .exceptions import ( RoborockTimeout, UnknownMethodError, ) from .roborock_future import RoborockFuture from .roborock_message import ( RoborockMessage, RoborockMessageProtocol, ) from .util import get_next_int _LOGGER = logging.getLogger(__name__) KEEPALIVE = 60 class RoborockClient(ABC): """Roborock client base class.""" _logger: logging.LoggerAdapter queue_timeout: int def __init__(self, device_info: DeviceData) -> None: """Initialize RoborockClient.""" self.device_info = device_info self._nonce = secrets.token_bytes(16) self._waiting_queue: dict[int, RoborockFuture] = {} self._last_device_msg_in = time.monotonic() self._last_disconnection = time.monotonic() self.keep_alive = KEEPALIVE self._diagnostic_data: dict[str, dict[str, Any]] = { "misc_info": {"Nonce": base64.b64encode(self._nonce).decode("utf-8")} } self.is_available: bool = True async def async_release(self) -> None: await self.async_disconnect() @property def diagnostic_data(self) -> dict: return self._diagnostic_data @abstractmethod async def async_connect(self): """Connect to the Roborock device.""" @abstractmethod async def async_disconnect(self) -> Any: """Disconnect from the Roborock device.""" @abstractmethod def is_connected(self) -> bool: """Return True if the client is connected to the device.""" @abstractmethod def on_message_received(self, messages: list[RoborockMessage]) -> None: """Handle received incoming messages from the device.""" def on_connection_lost(self, exc: Exception | None) -> None: self._last_disconnection = time.monotonic() self._logger.info("Roborock client disconnected") if exc is not None: self._logger.warning(exc) def should_keepalive(self) -> bool: now = time.monotonic() # noinspection PyUnresolvedReferences if now - self._last_disconnection > self.keep_alive**2 and now - self._last_device_msg_in > self.keep_alive: return False return True async def validate_connection(self) -> None: if not self.should_keepalive(): self._logger.info("Resetting Roborock connection due to kepalive timeout") await self.async_disconnect() await self.async_connect() async def _wait_response(self, request_id: int, queue: RoborockFuture) -> Any: try: response = await queue.async_get(self.queue_timeout) if response == "unknown_method": raise UnknownMethodError("Unknown method") return response except (asyncio.TimeoutError, asyncio.CancelledError): raise RoborockTimeout(f"id={request_id} Timeout after {self.queue_timeout} seconds") from None finally: self._waiting_queue.pop(request_id, None) def _async_response(self, request_id: int, protocol_id: int = 0) -> Any: queue = RoborockFuture(protocol_id) if request_id in self._waiting_queue and not ( request_id == 2 and protocol_id == RoborockMessageProtocol.PING_REQUEST ): new_id = get_next_int(10000, 32767) self._logger.warning( "Attempting to create a future with an existing id %s (%s)... New id is %s. " "Code may not function properly.", request_id, protocol_id, new_id, ) request_id = new_id self._waiting_queue[request_id] = queue return asyncio.ensure_future(self._wait_response(request_id, queue)) @abstractmethod async def send_message(self, roborock_message: RoborockMessage): """Send a message to the Roborock device.""" python-roborock-2.19.0/roborock/cli.py000066400000000000000000000215551501065527500177210ustar00rootroot00000000000000from __future__ import annotations import json import logging from pathlib import Path from typing import Any import click from pyshark import FileCapture # type: ignore from pyshark.capture.live_capture import LiveCapture, UnknownInterfaceException # type: ignore from pyshark.packet.packet import Packet # type: ignore from roborock import RoborockException from roborock.containers import DeviceData, HomeDataProduct, LoginData from roborock.protocol import MessageParser from roborock.util import run_sync from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1 from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 from roborock.web_api import RoborockApiClient _LOGGER = logging.getLogger(__name__) class RoborockContext: roborock_file = Path("~/.roborock").expanduser() _login_data: LoginData | None = None def __init__(self): self.reload() def reload(self): if self.roborock_file.is_file(): with open(self.roborock_file) as f: data = json.load(f) if data: self._login_data = LoginData.from_dict(data) def update(self, login_data: LoginData): data = json.dumps(login_data.as_dict(), default=vars) with open(self.roborock_file, "w") as f: f.write(data) self.reload() def validate(self): if self._login_data is None: raise RoborockException("You must login first") def login_data(self): self.validate() return self._login_data @click.option("-d", "--debug", default=False, count=True) @click.version_option(package_name="python-roborock") @click.group() @click.pass_context def cli(ctx, debug: int): logging_config: dict[str, Any] = {"level": logging.DEBUG if debug > 0 else logging.INFO} logging.basicConfig(**logging_config) # type: ignore ctx.obj = RoborockContext() @click.command() @click.option("--email", required=True) @click.option("--password", required=True) @click.pass_context @run_sync() async def login(ctx, email, password): """Login to Roborock account.""" context: RoborockContext = ctx.obj try: context.validate() _LOGGER.info("Already logged in") return except RoborockException: pass client = RoborockApiClient(email) user_data = await client.pass_login(password) context.update(LoginData(user_data=user_data, email=email)) async def _discover(ctx): context: RoborockContext = ctx.obj login_data = context.login_data() if not login_data: raise Exception("You need to login first") client = RoborockApiClient(login_data.email) home_data = await client.get_home_data(login_data.user_data) login_data.home_data = home_data context.update(login_data) click.echo(f"Discovered devices {', '.join([device.name for device in home_data.get_all_devices()])}") @click.command() @click.pass_context @run_sync() async def discover(ctx): await _discover(ctx) @click.command() @click.pass_context @run_sync() async def list_devices(ctx): context: RoborockContext = ctx.obj login_data = context.login_data() if not login_data.home_data: await _discover(ctx) login_data = context.login_data() home_data = login_data.home_data device_name_id = ", ".join( [f"{device.name}: {device.duid}" for device in home_data.devices + home_data.received_devices] ) click.echo(f"Known devices {device_name_id}") @click.command() @click.option("--device_id", required=True) @click.pass_context @run_sync() async def list_scenes(ctx, device_id): context: RoborockContext = ctx.obj login_data = context.login_data() if not login_data.home_data: await _discover(ctx) login_data = context.login_data() client = RoborockApiClient(login_data.email) scenes = await client.get_scenes(login_data.user_data, device_id) output_list = [] for scene in scenes: output_list.append(scene.as_dict()) click.echo(json.dumps(output_list, indent=4)) @click.command() @click.option("--scene_id", required=True) @click.pass_context @run_sync() async def execute_scene(ctx, scene_id): context: RoborockContext = ctx.obj login_data = context.login_data() if not login_data.home_data: await _discover(ctx) login_data = context.login_data() client = RoborockApiClient(login_data.email) await client.execute_scene(login_data.user_data, scene_id) @click.command() @click.option("--device_id", required=True) @click.pass_context @run_sync() async def status(ctx, device_id): context: RoborockContext = ctx.obj login_data = context.login_data() if not login_data.home_data: await _discover(ctx) login_data = context.login_data() home_data = login_data.home_data devices = home_data.devices + home_data.received_devices device = next(device for device in devices if device.duid == device_id) product_info: dict[str, HomeDataProduct] = {product.id: product for product in home_data.products} device_data = DeviceData(device, product_info[device.product_id].model) mqtt_client = RoborockMqttClientV1(login_data.user_data, device_data) networking = await mqtt_client.get_networking() local_device_data = DeviceData(device, product_info[device.product_id].model, networking.ip) local_client = RoborockLocalClientV1(local_device_data) status = await local_client.get_status() click.echo(json.dumps(status.as_dict(), indent=4)) @click.command() @click.option("--device_id", required=True) @click.option("--cmd", required=True) @click.option("--params", required=False) @click.pass_context @run_sync() async def command(ctx, cmd, device_id, params): context: RoborockContext = ctx.obj login_data = context.login_data() if not login_data.home_data: await _discover(ctx) login_data = context.login_data() home_data = login_data.home_data devices = home_data.devices + home_data.received_devices device = next(device for device in devices if device.duid == device_id) model = next( (product.model for product in home_data.products if device is not None and product.id == device.product_id), None, ) if model is None: raise RoborockException(f"Could not find model for device {device.name}") device_info = DeviceData(device=device, model=model) mqtt_client = RoborockMqttClientV1(login_data.user_data, device_info) await mqtt_client.send_command(cmd, json.loads(params) if params is not None else None) await mqtt_client.async_release() @click.command() @click.option("--local_key", required=True) @click.option("--device_ip", required=True) @click.option("--file", required=False) @click.pass_context @run_sync() async def parser(_, local_key, device_ip, file): file_provided = file is not None if file_provided: capture = FileCapture(file) else: _LOGGER.info("Listen for interface rvi0 since no file was provided") capture = LiveCapture(interface="rvi0") buffer = {"data": b""} def on_package(packet: Packet): if hasattr(packet, "ip"): if packet.transport_layer == "TCP" and (packet.ip.dst == device_ip or packet.ip.src == device_ip): if hasattr(packet, "DATA"): if hasattr(packet.DATA, "data"): if packet.ip.dst == device_ip: try: f, buffer["data"] = MessageParser.parse( buffer["data"] + bytes.fromhex(packet.DATA.data), local_key, ) print(f"Received request: {f}") except BaseException as e: print(e) pass elif packet.ip.src == device_ip: try: f, buffer["data"] = MessageParser.parse( buffer["data"] + bytes.fromhex(packet.DATA.data), local_key, ) print(f"Received response: {f}") except BaseException as e: print(e) pass try: await capture.packets_from_tshark(on_package, close_tshark=not file_provided) except UnknownInterfaceException: raise RoborockException( "You need to run 'rvictl -s XXXXXXXX-XXXXXXXXXXXXXXXX' first, with an iPhone connected to usb port" ) cli.add_command(login) cli.add_command(discover) cli.add_command(list_devices) cli.add_command(list_scenes) cli.add_command(execute_scene) cli.add_command(status) cli.add_command(command) cli.add_command(parser) def main(): return cli() if __name__ == "__main__": main() python-roborock-2.19.0/roborock/cloud_api.py000066400000000000000000000153651501065527500211130ustar00rootroot00000000000000from __future__ import annotations import asyncio import logging import threading from abc import ABC from asyncio import Lock from typing import Any from urllib.parse import urlparse import paho.mqtt.client as mqtt from .api import KEEPALIVE, RoborockClient from .containers import DeviceData, UserData from .exceptions import RoborockException, VacuumError from .protocol import MessageParser, md5hex from .roborock_future import RoborockFuture _LOGGER = logging.getLogger(__name__) CONNECT_REQUEST_ID = 0 DISCONNECT_REQUEST_ID = 1 class _Mqtt(mqtt.Client): """Internal MQTT client. This is a subclass of the Paho MQTT client that adds some additional functionality for error cases where things get stuck. """ _thread: threading.Thread def __init__(self) -> None: """Initialize the MQTT client.""" super().__init__(protocol=mqtt.MQTTv5) def maybe_restart_loop(self) -> None: """Ensure that the MQTT loop is running in case it previously exited.""" if not self._thread or not self._thread.is_alive(): if self._thread: _LOGGER.info("Stopping mqtt loop") super().loop_stop() _LOGGER.info("Starting mqtt loop") super().loop_start() class RoborockMqttClient(RoborockClient, ABC): """Roborock MQTT client base class.""" def __init__(self, user_data: UserData, device_info: DeviceData) -> None: """Initialize the Roborock MQTT client.""" rriot = user_data.rriot if rriot is None: raise RoborockException("Got no rriot data from user_data") RoborockClient.__init__(self, device_info) self._mqtt_user = rriot.u self._hashed_user = md5hex(self._mqtt_user + ":" + rriot.k)[2:10] url = urlparse(rriot.r.m) if not isinstance(url.hostname, str): raise RoborockException("Url parsing returned an invalid hostname") self._mqtt_host = str(url.hostname) self._mqtt_port = url.port self._mqtt_ssl = url.scheme == "ssl" self._mqtt_client = _Mqtt() self._mqtt_client.on_connect = self._mqtt_on_connect self._mqtt_client.on_message = self._mqtt_on_message self._mqtt_client.on_disconnect = self._mqtt_on_disconnect if self._mqtt_ssl: self._mqtt_client.tls_set() self._mqtt_password = rriot.s self._hashed_password = md5hex(self._mqtt_password + ":" + rriot.k)[16:] self._mqtt_client.username_pw_set(self._hashed_user, self._hashed_password) self._waiting_queue: dict[int, RoborockFuture] = {} self._mutex = Lock() def _mqtt_on_connect(self, *args, **kwargs): _, __, ___, rc, ____ = args connection_queue = self._waiting_queue.get(CONNECT_REQUEST_ID) if rc != mqtt.MQTT_ERR_SUCCESS: message = f"Failed to connect ({mqtt.error_string(rc)})" self._logger.error(message) if connection_queue: connection_queue.set_exception(VacuumError(message)) else: self._logger.debug("Failed to notify connect future, not in queue") return self._logger.info(f"Connected to mqtt {self._mqtt_host}:{self._mqtt_port}") topic = f"rr/m/o/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}" (result, mid) = self._mqtt_client.subscribe(topic) if result != 0: message = f"Failed to subscribe ({mqtt.error_string(rc)})" self._logger.error(message) if connection_queue: connection_queue.set_exception(VacuumError(message)) return self._logger.info(f"Subscribed to topic {topic}") if connection_queue: connection_queue.set_result(True) def _mqtt_on_message(self, *args, **kwargs): client, __, msg = args try: messages, _ = MessageParser.parse(msg.payload, local_key=self.device_info.device.local_key) super().on_message_received(messages) except Exception as ex: self._logger.exception(ex) def _mqtt_on_disconnect(self, *args, **kwargs): _, __, rc, ___ = args try: exc = RoborockException(mqtt.error_string(rc)) if rc != mqtt.MQTT_ERR_SUCCESS else None super().on_connection_lost(exc) connection_queue = self._waiting_queue.get(DISCONNECT_REQUEST_ID) if connection_queue: connection_queue.set_result(True) except Exception as ex: self._logger.exception(ex) def is_connected(self) -> bool: """Check if the mqtt client is connected.""" return self._mqtt_client.is_connected() def _sync_disconnect(self) -> Any: if not self.is_connected(): return None self._logger.info("Disconnecting from mqtt") disconnected_future = self._async_response(DISCONNECT_REQUEST_ID) rc = self._mqtt_client.disconnect() if rc == mqtt.MQTT_ERR_NO_CONN: disconnected_future.cancel() return None if rc != mqtt.MQTT_ERR_SUCCESS: disconnected_future.cancel() raise RoborockException(f"Failed to disconnect ({mqtt.error_string(rc)})") return disconnected_future def _sync_connect(self) -> Any: if self.is_connected(): self._mqtt_client.maybe_restart_loop() return None if self._mqtt_port is None or self._mqtt_host is None: raise RoborockException("Mqtt information was not entered. Cannot connect.") self._logger.debug("Connecting to mqtt") connected_future = self._async_response(CONNECT_REQUEST_ID) self._mqtt_client.connect(host=self._mqtt_host, port=self._mqtt_port, keepalive=KEEPALIVE) self._mqtt_client.maybe_restart_loop() return connected_future async def async_disconnect(self) -> None: async with self._mutex: if disconnected_future := self._sync_disconnect(): # There are no errors set on this future await disconnected_future loop = asyncio.get_running_loop() await loop.run_in_executor(None, self._mqtt_client.loop_stop) async def async_connect(self) -> None: async with self._mutex: if connected_future := self._sync_connect(): try: await connected_future except VacuumError as err: raise RoborockException(err) from err def _send_msg_raw(self, msg: bytes) -> None: info = self._mqtt_client.publish( f"rr/m/i/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}", msg ) if info.rc != mqtt.MQTT_ERR_SUCCESS: raise RoborockException(f"Failed to publish ({mqtt.error_string(info.rc)})") python-roborock-2.19.0/roborock/code_mappings.py000066400000000000000000000443101501065527500217540ustar00rootroot00000000000000from __future__ import annotations import logging from enum import Enum, IntEnum _LOGGER = logging.getLogger(__name__) completed_warnings = set() class RoborockEnum(IntEnum): """Roborock Enum for codes with int values""" @property def name(self) -> str: return super().name.lower() @classmethod def _missing_(cls: type[RoborockEnum], key) -> RoborockEnum: if hasattr(cls, "unknown"): warning = f"Missing {cls.__name__} code: {key} - defaulting to 'unknown'" if warning not in completed_warnings: completed_warnings.add(warning) _LOGGER.warning(warning) return cls.unknown # type: ignore default_value = next(item for item in cls) warning = f"Missing {cls.__name__} code: {key} - defaulting to {default_value}" if warning not in completed_warnings: completed_warnings.add(warning) _LOGGER.warning(warning) return default_value @classmethod def as_dict(cls: type[RoborockEnum]): return {i.name: i.value for i in cls if i.name != "missing"} @classmethod def as_enum_dict(cls: type[RoborockEnum]): return {i.value: i for i in cls if i.name != "missing"} @classmethod def values(cls: type[RoborockEnum]) -> list[int]: return list(cls.as_dict().values()) @classmethod def keys(cls: type[RoborockEnum]) -> list[str]: return list(cls.as_dict().keys()) @classmethod def items(cls: type[RoborockEnum]): return cls.as_dict().items() class RoborockStateCode(RoborockEnum): unknown = 0 starting = 1 charger_disconnected = 2 idle = 3 remote_control_active = 4 cleaning = 5 returning_home = 6 manual_mode = 7 charging = 8 charging_problem = 9 paused = 10 spot_cleaning = 11 error = 12 shutting_down = 13 updating = 14 docking = 15 going_to_target = 16 zoned_cleaning = 17 segment_cleaning = 18 emptying_the_bin = 22 # on s7+ washing_the_mop = 23 # on a46 washing_the_mop_2 = 25 going_to_wash_the_mop = 26 # on a46 in_call = 28 mapping = 29 egg_attack = 30 patrol = 32 attaching_the_mop = 33 # on g20s ultra detaching_the_mop = 34 # on g20s ultra charging_complete = 100 device_offline = 101 locked = 103 air_drying_stopping = 202 robot_status_mopping = 6301 clean_mop_cleaning = 6302 clean_mop_mopping = 6303 segment_mopping = 6304 segment_clean_mop_cleaning = 6305 segment_clean_mop_mopping = 6306 zoned_mopping = 6307 zoned_clean_mop_cleaning = 6308 zoned_clean_mop_mopping = 6309 back_to_dock_washing_duster = 6310 class RoborockDyadStateCode(RoborockEnum): unknown = -999 fetching = -998 # Obtaining Status fetch_failed = -997 # Failed to obtain device status. Try again later. updating = -996 washing = 1 ready = 2 charging = 3 mop_washing = 4 self_clean_cleaning = 5 self_clean_deep_cleaning = 6 self_clean_rinsing = 7 self_clean_dehydrating = 8 drying = 9 ventilating = 10 # drying reserving = 12 mop_washing_paused = 13 dusting_mode = 14 class RoborockErrorCode(RoborockEnum): none = 0 lidar_blocked = 1 bumper_stuck = 2 wheels_suspended = 3 cliff_sensor_error = 4 main_brush_jammed = 5 side_brush_jammed = 6 wheels_jammed = 7 robot_trapped = 8 no_dustbin = 9 strainer_error = 10 # Filter is wet or blocked compass_error = 11 # Strong magnetic field detected low_battery = 12 charging_error = 13 battery_error = 14 wall_sensor_dirty = 15 robot_tilted = 16 side_brush_error = 17 fan_error = 18 dock = 19 # Dock not connected to power optical_flow_sensor_dirt = 20 vertical_bumper_pressed = 21 dock_locator_error = 22 return_to_dock_fail = 23 nogo_zone_detected = 24 visual_sensor = 25 # Camera error light_touch = 26 # Wall sensor error vibrarise_jammed = 27 robot_on_carpet = 28 filter_blocked = 29 invisible_wall_detected = 30 cannot_cross_carpet = 31 internal_error = 32 collect_dust_error_3 = 34 # Clean auto-empty dock collect_dust_error_4 = 35 # Auto empty dock voltage error mopping_roller_1 = 36 # Wash roller may be jammed mopping_roller_error_2 = 37 # wash roller not lowered properly clear_water_box_hoare = 38 # Check the clean water tank dirty_water_box_hoare = 39 # Check the dirty water tank sink_strainer_hoare = 40 # Reinstall the water filter clear_water_box_exception = 41 # Clean water tank empty clear_brush_exception = 42 # Check that the water filter has been correctly installed clear_brush_exception_2 = 43 # Positioning button error filter_screen_exception = 44 # Clean the dock water filter mopping_roller_2 = 45 # Wash roller may be jammed up_water_exception = 48 drain_water_exception = 49 temperature_protection = 51 # Unit temperature protection clean_carousel_exception = 52 clean_carousel_water_full = 53 water_carriage_drop = 54 check_clean_carouse = 55 audio_error = 56 class RoborockFanPowerCode(RoborockEnum): """Describes the fan power of the vacuum cleaner.""" # Fan speeds should have the first letter capitalized - as there is no way to change the name in translations as # far as I am aware class RoborockFanSpeedV1(RoborockFanPowerCode): silent = 38 standard = 60 medium = 77 turbo = 90 class RoborockFanSpeedV2(RoborockFanPowerCode): silent = 101 balanced = 102 turbo = 103 max = 104 gentle = 105 auto = 106 class RoborockFanSpeedV3(RoborockFanPowerCode): silent = 38 standard = 60 medium = 75 turbo = 100 class RoborockFanSpeedE2(RoborockFanPowerCode): gentle = 41 silent = 50 standard = 68 medium = 79 turbo = 100 class RoborockFanSpeedS7(RoborockFanPowerCode): off = 105 quiet = 101 balanced = 102 turbo = 103 max = 104 custom = 106 class RoborockFanSpeedS7MaxV(RoborockFanPowerCode): off = 105 quiet = 101 balanced = 102 turbo = 103 max = 104 custom = 106 max_plus = 108 class RoborockFanSpeedS6Pure(RoborockFanPowerCode): gentle = 105 quiet = 101 balanced = 102 turbo = 103 max = 104 custom = 106 class RoborockFanSpeedQ7Max(RoborockFanPowerCode): quiet = 101 balanced = 102 turbo = 103 max = 104 class RoborockFanSpeedQRevoMaster(RoborockFanPowerCode): off = 105 quiet = 101 balanced = 102 turbo = 103 max = 104 custom = 106 max_plus = 108 smart_mode = 110 class RoborockFanSpeedQRevoCurv(RoborockFanPowerCode): quiet = 101 balanced = 102 turbo = 103 max = 104 off = 105 custom = 106 max_plus = 108 smart_mode = 110 class RoborockFanSpeedP10(RoborockFanPowerCode): off = 105 quiet = 101 balanced = 102 turbo = 103 max = 104 custom = 106 max_plus = 108 class RoborockFanSpeedS8MaxVUltra(RoborockFanPowerCode): off = 105 quiet = 101 balanced = 102 turbo = 103 max = 104 custom = 106 max_plus = 108 smart_mode = 110 class RoborockMopModeCode(RoborockEnum): """Describes the mop mode of the vacuum cleaner.""" class RoborockMopModeQRevoCurv(RoborockMopModeCode): standard = 300 deep = 301 custom = 302 deep_plus = 303 fast = 304 smart_mode = 306 class RoborockMopModeS7(RoborockMopModeCode): """Describes the mop mode of the vacuum cleaner.""" standard = 300 deep = 301 custom = 302 deep_plus = 303 class RoborockMopModeS8ProUltra(RoborockMopModeCode): standard = 300 deep = 301 deep_plus = 303 fast = 304 custom = 302 class RoborockMopModeS8MaxVUltra(RoborockMopModeCode): standard = 300 deep = 301 custom = 302 deep_plus = 303 fast = 304 deep_plus_pearl = 305 smart_mode = 306 class RoborockMopModeQRevoMaster(RoborockMopModeCode): standard = 300 deep = 301 custom = 302 deep_plus = 303 fast = 304 smart_mode = 306 class RoborockMopIntensityCode(RoborockEnum): """Describes the mop intensity of the vacuum cleaner.""" class RoborockMopIntensityS7(RoborockMopIntensityCode): """Describes the mop intensity of the vacuum cleaner.""" off = 200 mild = 201 moderate = 202 intense = 203 custom = 204 class RoborockMopIntensityV2(RoborockMopIntensityCode): """Describes the mop intensity of the vacuum cleaner.""" off = 200 low = 201 medium = 202 high = 203 custom = 207 class RoborockMopIntensityQRevoMaster(RoborockMopIntensityCode): """Describes the mop intensity of the vacuum cleaner.""" off = 200 low = 201 medium = 202 high = 203 custom = 204 custom_water_flow = 207 smart_mode = 209 class RoborockMopIntensityQRevoCurv(RoborockMopIntensityCode): off = 200 low = 201 medium = 202 high = 203 custom = 204 custom_water_flow = 207 smart_mode = 209 class RoborockMopIntensityP10(RoborockMopIntensityCode): """Describes the mop intensity of the vacuum cleaner.""" off = 200 low = 201 medium = 202 high = 203 custom = 204 custom_water_flow = 207 class RoborockMopIntensityS8MaxVUltra(RoborockMopIntensityCode): off = 200 low = 201 medium = 202 high = 203 custom = 204 max = 208 smart_mode = 209 custom_water_flow = 207 class RoborockMopIntensityS5Max(RoborockMopIntensityCode): """Describes the mop intensity of the vacuum cleaner.""" off = 200 low = 201 medium = 202 high = 203 custom = 204 custom_water_flow = 207 class RoborockMopIntensityS6MaxV(RoborockMopIntensityCode): """Describes the mop intensity of the vacuum cleaner.""" off = 200 low = 201 medium = 202 high = 203 custom = 204 custom_water_flow = 207 class RoborockMopIntensityQ7Max(RoborockMopIntensityCode): """Describes the mop intensity of the vacuum cleaner.""" off = 200 low = 201 medium = 202 high = 203 custom_water_flow = 207 class RoborockDockErrorCode(RoborockEnum): """Describes the error code of the dock.""" ok = 0 duct_blockage = 34 water_empty = 38 waste_water_tank_full = 39 maintenance_brush_jammed = 42 dirty_tank_latch_open = 44 no_dustbin = 46 cleaning_tank_full_or_blocked = 53 class RoborockDockTypeCode(RoborockEnum): unknown = -9999 no_dock = 0 auto_empty_dock = 1 empty_wash_fill_dock = 3 auto_empty_dock_pure = 5 s7_max_ultra_dock = 6 s8_dock = 7 p10_dock = 8 p10_pro_dock = 9 s8_maxv_ultra_dock = 10 qrevo_master_dock = 14 qrevo_s_dock = 15 saros_r10_dock = 16 qrevo_curv_dock = 17 saros_10_dock = 18 class RoborockDockDustCollectionModeCode(RoborockEnum): """Describes the dust collection mode of the vacuum cleaner.""" # TODO: Get the correct values for various different docks unknown = -9999 smart = 0 light = 1 balanced = 2 max = 4 class RoborockDockWashTowelModeCode(RoborockEnum): """Describes the wash towel mode of the vacuum cleaner.""" # TODO: Get the correct values for various different docks unknown = -9999 light = 0 balanced = 1 deep = 2 smart = 10 class RoborockCategory(Enum): """Describes the category of the device.""" WET_DRY_VAC = "roborock.wetdryvac" VACUUM = "robot.vacuum.cleaner" WASHING_MACHINE = "roborock.wm" UNKNOWN = "UNKNOWN" def __missing__(self, key): _LOGGER.warning("Missing key %s from category", key) return RoborockCategory.UNKNOWN class RoborockFinishReason(RoborockEnum): manual_interrupt = 21 # Cleaning interrupted by user cleanup_interrupted = 24 # Cleanup interrupted manual_interrupt_2 = 21 breakpoint = 32 # Could not continue cleaning breakpoint_2 = 33 cleanup_interrupted_2 = 34 manual_interrupt_3 = 35 manual_interrupt_4 = 36 manual_interrupt_5 = 37 manual_interrupt_6 = 43 locate_fail = 45 # Positioning Failed cleanup_interrupted_3 = 64 locate_fail_2 = 65 manual_interrupt_7 = 48 manual_interrupt_8 = 49 manual_interrupt_9 = 50 cleanup_interrupted_4 = 51 finished_cleaning = 52 # Finished cleaning finished_cleaning_2 = 54 finished_cleaning_3 = 55 finished_cleaning_4 = 56 finished_clenaing_5 = 57 manual_interrupt_10 = 60 area_unreachable = 61 # Area unreachable area_unreachable_2 = 62 washing_error = 67 # Washing error back_to_wash_failure = 68 # Failed to return to the dock cleanup_interrupted_5 = 101 breakpoint_4 = 102 manual_interrupt_11 = 103 cleanup_interrupted_6 = 104 cleanup_interrupted_7 = 105 cleanup_interrupted_8 = 106 cleanup_interrupted_9 = 107 cleanup_interrupted_10 = 109 cleanup_interrupted_11 = 110 patrol_success = 114 # Cruise completed patrol_fail = 115 # Cruise failed pet_patrol_success = 116 # Pet found pet_patrol_fail = 117 # Pet found failed class RoborockInCleaning(RoborockEnum): complete = 0 global_clean_not_complete = 1 zone_clean_not_complete = 2 segment_clean_not_complete = 3 class RoborockCleanType(RoborockEnum): all_zone = 1 draw_zone = 2 select_zone = 3 quick_build = 4 video_patrol = 5 pet_patrol = 6 class RoborockStartType(RoborockEnum): button = 1 app = 2 schedule = 3 mi_home = 4 quick_start = 5 voice_control = 13 routines = 101 alexa = 801 google = 802 ifttt = 803 yandex = 804 homekit = 805 xiaoai = 806 tmall_genie = 807 duer = 808 dingdong = 809 siri = 810 clova = 811 wechat = 901 alipay = 902 aqara = 903 hisense = 904 huawei = 905 widget_launch = 820 smart_watch = 821 class DyadSelfCleanMode(RoborockEnum): self_clean = 1 self_clean_and_dry = 2 dry = 3 ventilation = 4 class DyadSelfCleanLevel(RoborockEnum): normal = 1 deep = 2 class DyadWarmLevel(RoborockEnum): normal = 1 deep = 2 class DyadMode(RoborockEnum): wash = 1 wash_and_dry = 2 dry = 3 class DyadCleanMode(RoborockEnum): auto = 1 max = 2 dehydration = 3 power_saving = 4 class DyadSuction(RoborockEnum): l1 = 1 l2 = 2 l3 = 3 l4 = 4 l5 = 5 l6 = 6 class DyadWaterLevel(RoborockEnum): l1 = 1 l2 = 2 l3 = 3 l4 = 4 class DyadBrushSpeed(RoborockEnum): l1 = 1 l2 = 2 class DyadCleanser(RoborockEnum): none = 0 normal = 1 deep = 2 max = 3 class DyadError(RoborockEnum): none = 0 dirty_tank_full = 20000 # Dirty tank full. Empty it water_level_sensor_stuck = 20001 # Water level sensor is stuck. Clean it. clean_tank_empty = 20002 # Clean tank empty. Refill now clean_head_entangled = 20003 # Check if the cleaning head is entangled with foreign objects. clean_head_too_hot = 20004 # Cleaning head temperature protection. Wait for the temperature to return to normal. fan_protection_e5 = 10005 # Fan protection (E5). Restart the vacuum cleaner. cleaning_head_blocked = 20005 # Remove blockages from the cleaning head and pipes. temperature_protection = 20006 # Temperature protection. Wait for the temperature to return to normal fan_protection_e4 = 10004 # Fan protection (E4). Restart the vacuum cleaner. fan_protection_e9 = 10009 # Fan protection (E9). Restart the vacuum cleaner. battery_temperature_protection_e0 = 10000 battery_temperature_protection = ( 20007 # Battery temperature protection. Wait for the temperature to return to a normal range. ) battery_temperature_protection_2 = 20008 power_adapter_error = 20009 # Check if the power adapter is working properly. dirty_charging_contacts = 10007 # Disconnection between the device and dock. Wipe charging contacts. low_battery = 20017 # Low battery level. Charge before starting self-cleaning. battery_under_10 = 20018 # Charge until the battery level exceeds 10% before manually starting self-cleaning. class ZeoMode(RoborockEnum): wash = 1 wash_and_dry = 2 dry = 3 class ZeoState(RoborockEnum): standby = 1 weighing = 2 soaking = 3 washing = 4 rinsing = 5 spinning = 6 drying = 7 cooling = 8 under_delay_start = 9 done = 10 class ZeoProgram(RoborockEnum): standard = 1 quick = 2 sanitize = 3 wool = 4 air_refresh = 5 custom = 6 bedding = 7 down = 8 silk = 9 rinse_and_spin = 10 spin = 11 down_clean = 12 baby_care = 13 anti_allergen = 14 sportswear = 15 night = 16 new_clothes = 17 shirts = 18 synthetics = 19 underwear = 20 gentle = 21 intensive = 22 cotton_linen = 23 season = 24 warming = 25 bra = 26 panties = 27 boiling_wash = 28 socks = 30 towels = 31 anti_mite = 32 exo_40_60 = 33 twenty_c = 34 t_shirts = 35 stain_removal = 36 class ZeoSoak(RoborockEnum): normal = 0 low = 1 medium = 2 high = 3 max = 4 class ZeoTemperature(RoborockEnum): normal = 1 low = 2 medium = 3 high = 4 max = 5 twenty_c = 6 class ZeoRinse(RoborockEnum): none = 0 min = 1 low = 2 mid = 3 high = 4 max = 5 class ZeoSpin(RoborockEnum): none = 1 very_low = 2 low = 3 mid = 4 high = 5 very_high = 6 max = 7 class ZeoDryingMode(RoborockEnum): none = 0 quick = 1 iron = 2 store = 3 class ZeoDetergentType(RoborockEnum): empty = 0 low = 1 medium = 2 high = 3 class ZeoSoftenerType(RoborockEnum): empty = 0 low = 1 medium = 2 high = 3 class ZeoError(RoborockEnum): none = 0 refill_error = 1 drain_error = 2 door_lock_error = 3 water_level_error = 4 inverter_error = 5 heating_error = 6 temperature_error = 7 communication_error = 10 drying_error = 11 drying_error_e_12 = 12 drying_error_e_13 = 13 drying_error_e_14 = 14 drying_error_e_15 = 15 drying_error_e_16 = 16 drying_error_water_flow = 17 # Check for normal water flow drying_error_restart = 18 # Restart the washer and try again spin_error = 19 # re-arrange clothes python-roborock-2.19.0/roborock/command_cache.py000066400000000000000000000173501501065527500217110ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass, field from enum import Enum from roborock import RoborockCommand GET_PREFIX = "get_" SET_PREFIX = ("set_", "change_", "close_") class CacheableAttribute(str, Enum): status = "status" consumable = "consumable" sound_volume = "sound_volume" camera_status = "camera_status" carpet_clean_mode = "carpet_clean_mode" carpet_mode = "carpet_mode" child_lock_status = "child_lock_status" collision_avoid_status = "collision_avoid_status" customize_clean_mode = "customize_clean_mode" custom_mode = "custom_mode" dnd_timer = "dnd_timer" dust_collection_mode = "dust_collection_mode" flow_led_status = "flow_led_status" identify_furniture_status = "identify_furniture_status" identify_ground_material_status = "identify_ground_material_status" led_status = "led_status" server_timer = "server_timer" smart_wash_params = "smart_wash_params" timezone = "timezone" valley_electricity_timer = "valley_electricity_timer" wash_towel_mode = "wash_towel_mode" @dataclass class RoborockAttribute: attribute: str get_command: RoborockCommand add_command: RoborockCommand | None = None set_command: RoborockCommand | None = None close_command: RoborockCommand | None = None additional_change_commands: list[RoborockCommand] = field(default_factory=list) cache_map: Mapping[CacheableAttribute, RoborockAttribute] = { CacheableAttribute.status: RoborockAttribute( attribute="status", get_command=RoborockCommand.GET_STATUS, additional_change_commands=[ RoborockCommand.SET_WATER_BOX_CUSTOM_MODE, RoborockCommand.SET_MOP_MODE, ], ), CacheableAttribute.consumable: RoborockAttribute( attribute="consumable", get_command=RoborockCommand.GET_CONSUMABLE, ), CacheableAttribute.sound_volume: RoborockAttribute( attribute="sound_volume", get_command=RoborockCommand.GET_SOUND_VOLUME, set_command=RoborockCommand.CHANGE_SOUND_VOLUME, ), CacheableAttribute.camera_status: RoborockAttribute( attribute="camera_status", get_command=RoborockCommand.GET_CAMERA_STATUS, set_command=RoborockCommand.SET_CAMERA_STATUS, ), CacheableAttribute.carpet_clean_mode: RoborockAttribute( attribute="carpet_clean_mode", get_command=RoborockCommand.GET_CARPET_CLEAN_MODE, set_command=RoborockCommand.SET_CARPET_CLEAN_MODE, ), CacheableAttribute.carpet_mode: RoborockAttribute( attribute="carpet_mode", get_command=RoborockCommand.GET_CARPET_MODE, set_command=RoborockCommand.SET_CARPET_MODE, ), CacheableAttribute.child_lock_status: RoborockAttribute( attribute="child_lock_status", get_command=RoborockCommand.GET_CHILD_LOCK_STATUS, set_command=RoborockCommand.SET_CHILD_LOCK_STATUS, ), CacheableAttribute.collision_avoid_status: RoborockAttribute( attribute="collision_avoid_status", get_command=RoborockCommand.GET_COLLISION_AVOID_STATUS, set_command=RoborockCommand.SET_COLLISION_AVOID_STATUS, ), CacheableAttribute.customize_clean_mode: RoborockAttribute( attribute="customize_clean_mode", get_command=RoborockCommand.GET_CUSTOMIZE_CLEAN_MODE, set_command=RoborockCommand.SET_CUSTOMIZE_CLEAN_MODE, ), CacheableAttribute.custom_mode: RoborockAttribute( attribute="custom_mode", get_command=RoborockCommand.GET_CUSTOM_MODE, set_command=RoborockCommand.SET_CUSTOM_MODE, ), CacheableAttribute.dnd_timer: RoborockAttribute( attribute="dnd_timer", get_command=RoborockCommand.GET_DND_TIMER, set_command=RoborockCommand.SET_DND_TIMER, close_command=RoborockCommand.CLOSE_DND_TIMER, ), CacheableAttribute.dust_collection_mode: RoborockAttribute( attribute="dust_collection_mode", get_command=RoborockCommand.GET_DUST_COLLECTION_MODE, set_command=RoborockCommand.SET_DUST_COLLECTION_MODE, ), CacheableAttribute.flow_led_status: RoborockAttribute( attribute="flow_led_status", get_command=RoborockCommand.GET_FLOW_LED_STATUS, set_command=RoborockCommand.SET_FLOW_LED_STATUS, ), CacheableAttribute.identify_furniture_status: RoborockAttribute( attribute="identify_furniture_status", get_command=RoborockCommand.GET_IDENTIFY_FURNITURE_STATUS, set_command=RoborockCommand.SET_IDENTIFY_FURNITURE_STATUS, ), CacheableAttribute.identify_ground_material_status: RoborockAttribute( attribute="identify_ground_material_status", get_command=RoborockCommand.GET_IDENTIFY_GROUND_MATERIAL_STATUS, set_command=RoborockCommand.SET_IDENTIFY_GROUND_MATERIAL_STATUS, ), CacheableAttribute.led_status: RoborockAttribute( attribute="led_status", get_command=RoborockCommand.GET_LED_STATUS, set_command=RoborockCommand.SET_LED_STATUS, ), CacheableAttribute.server_timer: RoborockAttribute( attribute="server_timer", get_command=RoborockCommand.GET_SERVER_TIMER, add_command=RoborockCommand.SET_SERVER_TIMER, set_command=RoborockCommand.UPD_SERVER_TIMER, close_command=RoborockCommand.DEL_SERVER_TIMER, ), CacheableAttribute.smart_wash_params: RoborockAttribute( attribute="smart_wash_params", get_command=RoborockCommand.GET_SMART_WASH_PARAMS, set_command=RoborockCommand.SET_SMART_WASH_PARAMS, ), CacheableAttribute.timezone: RoborockAttribute( attribute="timezone", get_command=RoborockCommand.GET_TIMEZONE, set_command=RoborockCommand.SET_TIMEZONE ), CacheableAttribute.valley_electricity_timer: RoborockAttribute( attribute="valley_electricity_timer", get_command=RoborockCommand.GET_VALLEY_ELECTRICITY_TIMER, set_command=RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER, close_command=RoborockCommand.CLOSE_VALLEY_ELECTRICITY_TIMER, ), CacheableAttribute.wash_towel_mode: RoborockAttribute( attribute="wash_towel_mode", get_command=RoborockCommand.GET_WASH_TOWEL_MODE, set_command=RoborockCommand.SET_WASH_TOWEL_MODE, ), } def get_change_commands(attr: RoborockAttribute) -> list[RoborockCommand]: commands = [ attr.add_command, attr.set_command, attr.close_command, *attr.additional_change_commands, ] return [command for command in commands if command is not None] cache_map_by_get_command: dict[RoborockCommand | str, CacheableAttribute] = { attribute.get_command: cacheable_attribute for cacheable_attribute, attribute in cache_map.items() } cache_map_by_change_command: dict[RoborockCommand | str, CacheableAttribute] = { command: cacheable_attribute for cacheable_attribute, attribute in cache_map.items() for command in get_change_commands(attribute) } def get_cache_map(): return cache_map class CommandType(Enum): OTHER = -1 GET = 0 CHANGE = 1 @dataclass class CacheableAttributeResult: attribute: CacheableAttribute type: CommandType def find_cacheable_attribute(method: RoborockCommand | str) -> CacheableAttributeResult | None: if method is None: return None cacheable_attribute = None command_type = CommandType.OTHER if cacheable_attribute := cache_map_by_get_command.get(method, None): command_type = CommandType.GET elif cacheable_attribute := cache_map_by_change_command.get(method, None): command_type = CommandType.CHANGE if cacheable_attribute: return CacheableAttributeResult(attribute=CacheableAttribute(cacheable_attribute), type=command_type) else: return None python-roborock-2.19.0/roborock/const.py000066400000000000000000000052361501065527500202760ustar00rootroot00000000000000# Total time in seconds consumables have before Roborock recommends replacing MAIN_BRUSH_REPLACE_TIME = 1080000 SIDE_BRUSH_REPLACE_TIME = 720000 FILTER_REPLACE_TIME = 540000 SENSOR_DIRTY_REPLACE_TIME = 108000 MOP_ROLLER_REPLACE_TIME = 1080000 STRAINER_REPLACE_TIME = 540000 CLEANING_BRUSH_REPLACE_TIME = 1080000 DUST_COLLECTION_REPLACE_TIME = 81000 FLOOR_CLEANER_REPLACE_TIME = 1080000 ROBOROCK_V1 = "ROBOROCK.vacuum.v1" ROBOROCK_S4 = "roborock.vacuum.s4" ROBOROCK_S4_MAX = "roborock.vacuum.a19" ROBOROCK_S5 = "roborock.vacuum.s5" ROBOROCK_S5_MAX = "roborock.vacuum.s5e" ROBOROCK_S6 = "roborock.vacuum.s6" ROBOROCK_T6 = "roborock.vacuum.t6" # cn s6 ROBOROCK_E4 = "roborock.vacuum.a01" ROBOROCK_S6_PURE = "roborock.vacuum.a08" ROBOROCK_T7 = "roborock.vacuum.a11" # cn s7 ROBOROCK_T7S = "roborock.vacuum.a14" ROBOROCK_T7SPLUS = "roborock.vacuum.a23" ROBOROCK_S7_MAXV = "roborock.vacuum.a27" ROBOROCK_S7_MAXV_ULTRA = "roborock.vacuum.a65" ROBOROCK_S7_PRO_ULTRA = "roborock.vacuum.a62" ROBOROCK_Q5 = "roborock.vacuum.a34" ROBOROCK_Q5_PRO = "roborock.vacuum.a72" ROBOROCK_Q7 = "roborock.vacuum.a40" ROBOROCK_Q7_MAX = "roborock.vacuum.a38" ROBOROCK_Q7PLUS = "roborock.vacuum.a40" ROBOROCK_QREVO_MASTER = "roborock.vacuum.a117" ROBOROCK_QREVO_CURV = "roborock.vacuum.a135" ROBOROCK_Q8_MAX = "roborock.vacuum.a73" ROBOROCK_G10S_PRO = "roborock.vacuum.a26" ROBOROCK_G20S_Ultra = "roborock.vacuum.a143" # cn saros_r10 ROBOROCK_G10S = "roborock.vacuum.a46" ROBOROCK_G10 = "roborock.vacuum.a29" ROCKROBO_G10_SG = "roborock.vacuum.a30" # Variant of the G10, has similar features as S7 ROBOROCK_S7 = "roborock.vacuum.a15" ROBOROCK_S6_MAXV = "roborock.vacuum.a10" ROBOROCK_E2 = "roborock.vacuum.e2" ROBOROCK_1S = "roborock.vacuum.m1s" ROBOROCK_C1 = "roborock.vacuum.c1" ROBOROCK_S8_PRO_ULTRA = "roborock.vacuum.a70" ROBOROCK_S8 = "roborock.vacuum.a51" ROBOROCK_P10 = "roborock.vacuum.a75" # also known as q_revo ROBOROCK_S8_MAXV_ULTRA = "roborock.vacuum.a97" ROBOROCK_QREVO_S = "roborock.vacuum.a104" ROBOROCK_QREVO_PRO = "roborock.vacuum.a101" ROBOROCK_QREVO_MAXV = "roborock.vacuum.a87" ROBOROCK_DYAD_AIR = "roborock.wetdryvac.a107" ROBOROCK_DYAD_PRO_COMBO = "roborock.wetdryvac.a83" ROBOROCK_DYAD_PRO = "roborock.wetdryvac.a56" # These are the devices that show up when you add a device - more could be supported and just not show up SUPPORTED_VACUUMS = [ ROBOROCK_G10, ROBOROCK_G10S_PRO, ROBOROCK_G20S_Ultra, ROBOROCK_Q5, ROBOROCK_Q7, ROBOROCK_Q7_MAX, ROBOROCK_S4, ROBOROCK_S5_MAX, ROBOROCK_S6, ROBOROCK_S6_MAXV, ROBOROCK_S6_PURE, ROBOROCK_S7_MAXV, ROBOROCK_S8_PRO_ULTRA, ROBOROCK_S8, ROBOROCK_S4_MAX, ROBOROCK_S7, ROBOROCK_P10, ROCKROBO_G10_SG, ] python-roborock-2.19.0/roborock/containers.py000066400000000000000000000766631501065527500213310ustar00rootroot00000000000000from __future__ import annotations import datetime import json import logging import re from dataclasses import asdict, dataclass, field from datetime import timezone from enum import Enum from typing import Any, NamedTuple, get_args, get_origin from .code_mappings import ( RoborockCategory, RoborockCleanType, RoborockDockDustCollectionModeCode, RoborockDockErrorCode, RoborockDockTypeCode, RoborockDockWashTowelModeCode, RoborockErrorCode, RoborockFanPowerCode, RoborockFanSpeedP10, RoborockFanSpeedQ7Max, RoborockFanSpeedQRevoCurv, RoborockFanSpeedQRevoMaster, RoborockFanSpeedS6Pure, RoborockFanSpeedS7, RoborockFanSpeedS7MaxV, RoborockFanSpeedS8MaxVUltra, RoborockFinishReason, RoborockInCleaning, RoborockMopIntensityCode, RoborockMopIntensityP10, RoborockMopIntensityQ7Max, RoborockMopIntensityQRevoCurv, RoborockMopIntensityQRevoMaster, RoborockMopIntensityS5Max, RoborockMopIntensityS6MaxV, RoborockMopIntensityS7, RoborockMopIntensityS8MaxVUltra, RoborockMopModeCode, RoborockMopModeQRevoCurv, RoborockMopModeQRevoMaster, RoborockMopModeS7, RoborockMopModeS8MaxVUltra, RoborockMopModeS8ProUltra, RoborockStartType, RoborockStateCode, ) from .const import ( CLEANING_BRUSH_REPLACE_TIME, DUST_COLLECTION_REPLACE_TIME, FILTER_REPLACE_TIME, MAIN_BRUSH_REPLACE_TIME, MOP_ROLLER_REPLACE_TIME, ROBOROCK_G10S_PRO, ROBOROCK_P10, ROBOROCK_Q7_MAX, ROBOROCK_QREVO_CURV, ROBOROCK_QREVO_MASTER, ROBOROCK_QREVO_MAXV, ROBOROCK_QREVO_PRO, ROBOROCK_QREVO_S, ROBOROCK_S4_MAX, ROBOROCK_S5_MAX, ROBOROCK_S6, ROBOROCK_S6_MAXV, ROBOROCK_S6_PURE, ROBOROCK_S7, ROBOROCK_S7_MAXV, ROBOROCK_S8, ROBOROCK_S8_MAXV_ULTRA, ROBOROCK_S8_PRO_ULTRA, SENSOR_DIRTY_REPLACE_TIME, SIDE_BRUSH_REPLACE_TIME, STRAINER_REPLACE_TIME, ROBOROCK_G20S_Ultra, ) from .exceptions import RoborockException _LOGGER = logging.getLogger(__name__) def camelize(s: str): first, *others = s.split("_") if len(others) == 0: return s return "".join([first.lower(), *map(str.title, others)]) def decamelize(s: str): return re.sub("([A-Z]+)", "_\\1", s).lower() def decamelize_obj(d: dict | list, ignore_keys: list[str]): if isinstance(d, RoborockBase): d = d.as_dict() if isinstance(d, list): return [decamelize_obj(i, ignore_keys) if isinstance(i, dict | list) else i for i in d] return { (decamelize(a) if a not in ignore_keys else a): decamelize_obj(b, ignore_keys) if isinstance(b, dict | list) else b for a, b in d.items() } @dataclass class RoborockBase: _ignore_keys = [] # type: ignore is_cached = False @staticmethod def convert_to_class_obj(type, value): try: class_type = eval(type) if get_origin(class_type) is list: return_list = [] cls_type = get_args(class_type)[0] for obj in value: if issubclass(cls_type, RoborockBase): return_list.append(cls_type.from_dict(obj)) elif cls_type in {str, int, float}: return_list.append(cls_type(obj)) else: return_list.append(cls_type(**obj)) return return_list if issubclass(class_type, RoborockBase): converted_value = class_type.from_dict(value) else: converted_value = class_type(value) return converted_value except NameError as err: _LOGGER.exception(err) except ValueError as err: _LOGGER.exception(err) except Exception as err: _LOGGER.exception(err) raise Exception("Fail") @classmethod def from_dict(cls, data: dict[str, Any]): if isinstance(data, dict): ignore_keys = cls._ignore_keys data = decamelize_obj(data, ignore_keys) cls_annotations: dict[str, str] = {} for base in reversed(cls.__mro__): cls_annotations.update(getattr(base, "__annotations__", {})) remove_keys = [] for key, value in data.items(): if key not in cls_annotations: remove_keys.append(key) continue if value == "None" or value is None: data[key] = None continue field_type: str = cls_annotations[key] if "|" in field_type: # It's a union types = field_type.split("|") for type in types: if "None" in type or "Any" in type: continue try: data[key] = RoborockBase.convert_to_class_obj(type, value) break except Exception: ... else: try: data[key] = RoborockBase.convert_to_class_obj(field_type, value) except Exception: ... for key in remove_keys: del data[key] return cls(**data) def as_dict(self) -> dict: return asdict( self, dict_factory=lambda _fields: { camelize(key): value.value if isinstance(value, Enum) else value for (key, value) in _fields if value is not None }, ) @dataclass class RoborockBaseTimer(RoborockBase): start_hour: int | None = None start_minute: int | None = None end_hour: int | None = None end_minute: int | None = None enabled: int | None = None start_time: datetime.time | None = None end_time: datetime.time | None = None def __post_init__(self) -> None: self.start_time = ( datetime.time(hour=self.start_hour, minute=self.start_minute) if self.start_hour is not None and self.start_minute is not None else None ) self.end_time = ( datetime.time(hour=self.end_hour, minute=self.end_minute) if self.end_hour is not None and self.end_minute is not None else None ) @dataclass class Reference(RoborockBase): r: str | None = None a: str | None = None m: str | None = None l: str | None = None @dataclass class RRiot(RoborockBase): u: str s: str h: str k: str r: Reference @dataclass class UserData(RoborockBase): rriot: RRiot uid: int | None = None tokentype: str | None = None token: str | None = None rruid: str | None = None region: str | None = None countrycode: str | None = None country: str | None = None nickname: str | None = None tuya_device_state: int | None = None avatarurl: str | None = None @dataclass class HomeDataProductSchema(RoborockBase): id: Any | None = None name: Any | None = None code: Any | None = None mode: Any | None = None type: Any | None = None product_property: Any | None = None property: Any | None = None desc: Any | None = None @dataclass class HomeDataProduct(RoborockBase): id: str name: str model: str category: RoborockCategory code: str | None = None icon_url: str | None = None attribute: Any | None = None capability: int | None = None schema: list[HomeDataProductSchema] | None = None @dataclass class HomeDataDevice(RoborockBase): duid: str name: str local_key: str fv: str product_id: str attribute: Any | None = None active_time: int | None = None runtime_env: Any | None = None time_zone_id: str | None = None icon_url: str | None = None lon: Any | None = None lat: Any | None = None share: Any | None = None share_time: Any | None = None online: bool | None = None pv: str | None = None room_id: Any | None = None tuya_uuid: Any | None = None tuya_migrated: bool | None = None extra: Any | None = None sn: str | None = None feature_set: str | None = None new_feature_set: str | None = None device_status: dict | None = None silent_ota_switch: bool | None = None setting: Any | None = None f: bool | None = None device_features: DeviceFeatures | None = None # seemingly not just str like I thought - example: '0000000000002000' and '0000000000002F63' # def __post_init__(self): # if self.feature_set is not None and self.new_feature_set is not None and self.new_feature_set != "": # self.device_features = build_device_features(self.feature_set, self.new_feature_set) @dataclass class DeviceFeatures(RoborockBase): map_carpet_add_supported: bool show_clean_finish_reason_supported: bool resegment_supported: bool video_monitor_supported: bool any_state_transit_goto_supported: bool fw_filter_obstacle_supported: bool video_setting_supported: bool ignore_unknown_map_object_supported: bool set_child_supported: bool carpet_supported: bool mop_path_supported: bool multi_map_segment_timer_supported: bool custom_water_box_distance_supported: bool wash_then_charge_cmd_supported: bool room_name_supported: bool current_map_restore_enabled: bool photo_upload_supported: bool shake_mop_set_supported: bool map_beautify_internal_debug_supported: bool new_data_for_clean_history: bool new_data_for_clean_history_detail: bool flow_led_setting_supported: bool dust_collection_setting_supported: bool rpc_retry_supported: bool avoid_collision_supported: bool support_set_switch_map_mode: bool support_smart_scene: bool support_floor_edit: bool support_furniture: bool support_room_tag: bool support_quick_map_builder: bool support_smart_global_clean_with_custom_mode: bool record_allowed: bool careful_slow_map_supported: bool egg_mode_supported: bool unsave_map_reason_supported: bool carpet_show_on_map: bool supported_valley_electricity: bool drying_supported: bool download_test_voice_supported: bool support_backup_map: bool support_custom_mode_in_cleaning: bool support_remote_control_in_call: bool support_set_volume_in_call: bool support_clean_estimate: bool support_custom_dnd: bool carpet_deep_clean_supported: bool stuck_zone_supported: bool custom_door_sill_supported: bool clean_route_fast_mode_supported: bool cliff_zone_supported: bool smart_door_sill_supported: bool support_floor_direction: bool wifi_manage_supported: bool back_charge_auto_wash_supported: bool support_incremental_map: bool offline_map_supported: bool def build_device_features(feature_set: str, new_feature_set: str) -> DeviceFeatures: new_feature_set_int = int(new_feature_set) feature_set_int = int(feature_set) new_feature_set_divided = int(new_feature_set_int / (2**32)) # Convert last 8 digits of new feature set into hexadecimal number converted_new_feature_set = int("0x" + new_feature_set[-8:], 16) new_feature_set_mod_8: bool = len(new_feature_set) % 8 == 0 return DeviceFeatures( map_carpet_add_supported=bool(1073741824 & new_feature_set_int), show_clean_finish_reason_supported=bool(1 & new_feature_set_int), resegment_supported=bool(4 & new_feature_set_int), video_monitor_supported=bool(8 & new_feature_set_int), any_state_transit_goto_supported=bool(16 & new_feature_set_int), fw_filter_obstacle_supported=bool(32 & new_feature_set_int), video_setting_supported=bool(64 & new_feature_set_int), ignore_unknown_map_object_supported=bool(128 & new_feature_set_int), set_child_supported=bool(256 & new_feature_set_int), carpet_supported=bool(512 & new_feature_set_int), mop_path_supported=bool(2048 & new_feature_set_int), multi_map_segment_timer_supported=bool(feature_set_int and 4096 & new_feature_set_int), custom_water_box_distance_supported=bool(new_feature_set_int and 2147483648 & new_feature_set_int), wash_then_charge_cmd_supported=bool((new_feature_set_divided >> 5) & 1), room_name_supported=bool(16384 & new_feature_set_int), current_map_restore_enabled=bool(8192 & new_feature_set_int), photo_upload_supported=bool(65536 & new_feature_set_int), shake_mop_set_supported=bool(262144 & new_feature_set_int), map_beautify_internal_debug_supported=bool(2097152 & new_feature_set_int), new_data_for_clean_history=bool(4194304 & new_feature_set_int), new_data_for_clean_history_detail=bool(8388608 & new_feature_set_int), flow_led_setting_supported=bool(16777216 & new_feature_set_int), dust_collection_setting_supported=bool(33554432 & new_feature_set_int), rpc_retry_supported=bool(67108864 & new_feature_set_int), avoid_collision_supported=bool(134217728 & new_feature_set_int), support_set_switch_map_mode=bool(268435456 & new_feature_set_int), support_smart_scene=bool(new_feature_set_divided & 2), support_floor_edit=bool(new_feature_set_divided & 8), support_furniture=bool((new_feature_set_divided >> 4) & 1), support_room_tag=bool((new_feature_set_divided >> 6) & 1), support_quick_map_builder=bool((new_feature_set_divided >> 7) & 1), support_smart_global_clean_with_custom_mode=bool((new_feature_set_divided >> 8) & 1), record_allowed=bool(1024 & new_feature_set_int), careful_slow_map_supported=bool((new_feature_set_divided >> 9) & 1), egg_mode_supported=bool((new_feature_set_divided >> 10) & 1), unsave_map_reason_supported=bool((new_feature_set_divided >> 14) & 1), carpet_show_on_map=bool((new_feature_set_divided >> 12) & 1), supported_valley_electricity=bool((new_feature_set_divided >> 13) & 1), # This one could actually be incorrect # ((t.robotNewFeatures / 2 ** 32) >> 15) & 1 && (module422.DMM.isTopazSV_CE || 'cn' == t.deviceLocation)); drying_supported=bool((new_feature_set_divided >> 15) & 1), download_test_voice_supported=bool((new_feature_set_divided >> 16) & 1), support_backup_map=bool((new_feature_set_divided >> 17) & 1), support_custom_mode_in_cleaning=bool((new_feature_set_divided >> 18) & 1), support_remote_control_in_call=bool((new_feature_set_divided >> 19) & 1), support_set_volume_in_call=new_feature_set_mod_8 and bool(1 & converted_new_feature_set), support_clean_estimate=new_feature_set_mod_8 and bool(2 & converted_new_feature_set), support_custom_dnd=new_feature_set_mod_8 and bool(4 & converted_new_feature_set), carpet_deep_clean_supported=bool(8 & converted_new_feature_set), stuck_zone_supported=new_feature_set_mod_8 and bool(16 & converted_new_feature_set), custom_door_sill_supported=new_feature_set_mod_8 and bool(32 & converted_new_feature_set), clean_route_fast_mode_supported=bool(256 & converted_new_feature_set), cliff_zone_supported=new_feature_set_mod_8 and bool(512 & converted_new_feature_set), smart_door_sill_supported=new_feature_set_mod_8 and bool(1024 & converted_new_feature_set), support_floor_direction=new_feature_set_mod_8 and bool(2048 & converted_new_feature_set), wifi_manage_supported=bool(128 & converted_new_feature_set), back_charge_auto_wash_supported=bool(4096 & converted_new_feature_set), support_incremental_map=bool(8192 & converted_new_feature_set), offline_map_supported=bool(16384 & converted_new_feature_set), ) @dataclass class HomeDataRoom(RoborockBase): id: int name: str @dataclass class HomeDataScene(RoborockBase): id: int name: str @dataclass class HomeData(RoborockBase): id: int name: str products: list[HomeDataProduct] = field(default_factory=lambda: []) devices: list[HomeDataDevice] = field(default_factory=lambda: []) received_devices: list[HomeDataDevice] = field(default_factory=lambda: []) lon: Any | None = None lat: Any | None = None geo_name: Any | None = None rooms: list[HomeDataRoom] = field(default_factory=list) def get_all_devices(self) -> list[HomeDataDevice]: devices = [] if self.devices is not None: devices += self.devices if self.received_devices is not None: devices += self.received_devices return devices @dataclass class LoginData(RoborockBase): user_data: UserData email: str home_data: HomeData | None = None @dataclass class Status(RoborockBase): msg_ver: int | None = None msg_seq: int | None = None state: RoborockStateCode | None = None battery: int | None = None clean_time: int | None = None clean_area: int | None = None square_meter_clean_area: float | None = None error_code: RoborockErrorCode | None = None map_present: int | None = None in_cleaning: RoborockInCleaning | None = None in_returning: int | None = None in_fresh_state: int | None = None lab_status: int | None = None water_box_status: int | None = None back_type: int | None = None wash_phase: int | None = None wash_ready: int | None = None fan_power: RoborockFanPowerCode | None = None dnd_enabled: int | None = None map_status: int | None = None is_locating: int | None = None lock_status: int | None = None water_box_mode: RoborockMopIntensityCode | None = None water_box_carriage_status: int | None = None mop_forbidden_enable: int | None = None camera_status: int | None = None is_exploring: int | None = None home_sec_status: int | None = None home_sec_enable_password: int | None = None adbumper_status: list[int] | None = None water_shortage_status: int | None = None dock_type: RoborockDockTypeCode | None = None dust_collection_status: int | None = None auto_dust_collection: int | None = None avoid_count: int | None = None mop_mode: RoborockMopModeCode | None = None debug_mode: int | None = None collision_avoid_status: int | None = None switch_map_mode: int | None = None dock_error_status: RoborockDockErrorCode | None = None charge_status: int | None = None unsave_map_reason: int | None = None unsave_map_flag: int | None = None wash_status: int | None = None distance_off: int | None = None in_warmup: int | None = None dry_status: int | None = None rdt: int | None = None clean_percent: int | None = None rss: int | None = None dss: int | None = None common_status: int | None = None corner_clean_mode: int | None = None error_code_name: str | None = None state_name: str | None = None water_box_mode_name: str | None = None fan_power_options: list[str] = field(default_factory=list) fan_power_name: str | None = None mop_mode_name: str | None = None def __post_init__(self) -> None: self.square_meter_clean_area = round(self.clean_area / 1000000, 1) if self.clean_area is not None else None if self.error_code is not None: self.error_code_name = self.error_code.name if self.state is not None: self.state_name = self.state.name if self.water_box_mode is not None: self.water_box_mode_name = self.water_box_mode.name if self.fan_power is not None: self.fan_power_options = self.fan_power.keys() self.fan_power_name = self.fan_power.name if self.mop_mode is not None: self.mop_mode_name = self.mop_mode.name def get_fan_speed_code(self, fan_speed: str) -> int: if self.fan_power is None: raise RoborockException("Attempted to get fan speed before status has been updated.") return self.fan_power.as_dict().get(fan_speed) def get_mop_intensity_code(self, mop_intensity: str) -> int: if self.water_box_mode is None: raise RoborockException("Attempted to get mop_intensity before status has been updated.") return self.water_box_mode.as_dict().get(mop_intensity) def get_mop_mode_code(self, mop_mode: str) -> int: if self.mop_mode is None: raise RoborockException("Attempted to get mop_mode before status has been updated.") return self.mop_mode.as_dict().get(mop_mode) @dataclass class S4MaxStatus(Status): fan_power: RoborockFanSpeedS6Pure | None = None water_box_mode: RoborockMopIntensityS7 | None = None mop_mode: RoborockMopModeS7 | None = None @dataclass class S5MaxStatus(Status): fan_power: RoborockFanSpeedS6Pure | None = None water_box_mode: RoborockMopIntensityS5Max | None = None @dataclass class Q7MaxStatus(Status): fan_power: RoborockFanSpeedQ7Max | None = None water_box_mode: RoborockMopIntensityQ7Max | None = None @dataclass class QRevoMasterStatus(Status): fan_power: RoborockFanSpeedQRevoMaster | None = None water_box_mode: RoborockMopIntensityQRevoMaster | None = None mop_mode: RoborockMopModeQRevoMaster | None = None @dataclass class QRevoCurvStatus(Status): fan_power: RoborockFanSpeedQRevoCurv | None = None water_box_mode: RoborockMopIntensityQRevoCurv | None = None mop_mode: RoborockMopModeQRevoCurv | None = None @dataclass class S6MaxVStatus(Status): fan_power: RoborockFanSpeedS7MaxV | None = None water_box_mode: RoborockMopIntensityS6MaxV | None = None @dataclass class S6PureStatus(Status): fan_power: RoborockFanSpeedS6Pure | None = None @dataclass class S7MaxVStatus(Status): fan_power: RoborockFanSpeedS7MaxV | None = None water_box_mode: RoborockMopIntensityS7 | None = None mop_mode: RoborockMopModeS7 | None = None @dataclass class S7Status(Status): fan_power: RoborockFanSpeedS7 | None = None water_box_mode: RoborockMopIntensityS7 | None = None mop_mode: RoborockMopModeS7 | None = None @dataclass class S8ProUltraStatus(Status): fan_power: RoborockFanSpeedS7MaxV | None = None water_box_mode: RoborockMopIntensityS7 | None = None mop_mode: RoborockMopModeS8ProUltra | None = None @dataclass class S8Status(Status): fan_power: RoborockFanSpeedS7MaxV | None = None water_box_mode: RoborockMopIntensityS7 | None = None mop_mode: RoborockMopModeS8ProUltra | None = None @dataclass class P10Status(Status): fan_power: RoborockFanSpeedP10 | None = None water_box_mode: RoborockMopIntensityP10 | None = None mop_mode: RoborockMopModeS8ProUltra | None = None @dataclass class S8MaxvUltraStatus(Status): fan_power: RoborockFanSpeedS8MaxVUltra | None = None water_box_mode: RoborockMopIntensityS8MaxVUltra | None = None mop_mode: RoborockMopModeS8MaxVUltra | None = None ModelStatus: dict[str, type[Status]] = { ROBOROCK_S4_MAX: S4MaxStatus, ROBOROCK_S5_MAX: S5MaxStatus, ROBOROCK_Q7_MAX: Q7MaxStatus, ROBOROCK_QREVO_MASTER: QRevoMasterStatus, ROBOROCK_QREVO_CURV: QRevoCurvStatus, ROBOROCK_S6: S6PureStatus, ROBOROCK_S6_MAXV: S6MaxVStatus, ROBOROCK_S6_PURE: S6PureStatus, ROBOROCK_S7_MAXV: S7MaxVStatus, ROBOROCK_S7: S7Status, ROBOROCK_S8: S8Status, ROBOROCK_S8_PRO_ULTRA: S8ProUltraStatus, ROBOROCK_G10S_PRO: S7MaxVStatus, ROBOROCK_G20S_Ultra: QRevoMasterStatus, ROBOROCK_P10: P10Status, # These likely are not correct, # but i am currently unable to do my typical reverse engineering/ get any data from users on this, # so this will be here in the mean time. ROBOROCK_QREVO_S: P10Status, ROBOROCK_QREVO_MAXV: P10Status, ROBOROCK_QREVO_PRO: P10Status, ROBOROCK_S8_MAXV_ULTRA: S8MaxvUltraStatus, } @dataclass class DnDTimer(RoborockBaseTimer): """DnDTimer""" @dataclass class ValleyElectricityTimer(RoborockBaseTimer): """ValleyElectricityTimer""" @dataclass class CleanSummary(RoborockBase): clean_time: int | None = None clean_area: int | None = None square_meter_clean_area: float | None = None clean_count: int | None = None dust_collection_count: int | None = None records: list[int] | None = None last_clean_t: int | None = None def __post_init__(self) -> None: if isinstance(self.clean_area, list | str): _LOGGER.warning(f"Clean area is a unexpected type! Please give the following in a issue: {self.clean_area}") else: self.square_meter_clean_area = round(self.clean_area / 1000000, 1) if self.clean_area is not None else None @dataclass class CleanRecord(RoborockBase): begin: int | None = None begin_datetime: datetime.datetime | None = None end: int | None = None end_datetime: datetime.datetime | None = None duration: int | None = None area: int | None = None square_meter_area: float | None = None error: int | None = None complete: int | None = None start_type: RoborockStartType | None = None clean_type: RoborockCleanType | None = None finish_reason: RoborockFinishReason | None = None dust_collection_status: int | None = None avoid_count: int | None = None wash_count: int | None = None map_flag: int | None = None def __post_init__(self) -> None: self.square_meter_area = round(self.area / 1000000, 1) if self.area is not None else None self.begin_datetime = ( datetime.datetime.fromtimestamp(self.begin).astimezone(timezone.utc) if self.begin else None ) self.end_datetime = datetime.datetime.fromtimestamp(self.end).astimezone(timezone.utc) if self.end else None @dataclass class Consumable(RoborockBase): main_brush_work_time: int | None = None side_brush_work_time: int | None = None filter_work_time: int | None = None filter_element_work_time: int | None = None sensor_dirty_time: int | None = None strainer_work_times: int | None = None dust_collection_work_times: int | None = None cleaning_brush_work_times: int | None = None moproller_work_time: int | None = None main_brush_time_left: int | None = None side_brush_time_left: int | None = None filter_time_left: int | None = None sensor_time_left: int | None = None strainer_time_left: int | None = None dust_collection_time_left: int | None = None cleaning_brush_time_left: int | None = None mop_roller_time_left: int | None = None def __post_init__(self) -> None: self.main_brush_time_left = ( MAIN_BRUSH_REPLACE_TIME - self.main_brush_work_time if self.main_brush_work_time is not None else None ) self.side_brush_time_left = ( SIDE_BRUSH_REPLACE_TIME - self.side_brush_work_time if self.side_brush_work_time is not None else None ) self.filter_time_left = ( FILTER_REPLACE_TIME - self.filter_work_time if self.filter_work_time is not None else None ) self.sensor_time_left = ( SENSOR_DIRTY_REPLACE_TIME - self.sensor_dirty_time if self.sensor_dirty_time is not None else None ) self.strainer_time_left = ( STRAINER_REPLACE_TIME - self.strainer_work_times if self.strainer_work_times is not None else None ) self.dust_collection_time_left = ( DUST_COLLECTION_REPLACE_TIME - self.dust_collection_work_times if self.dust_collection_work_times is not None else None ) self.cleaning_brush_time_left = ( CLEANING_BRUSH_REPLACE_TIME - self.cleaning_brush_work_times if self.cleaning_brush_work_times is not None else None ) self.mop_roller_time_left = ( MOP_ROLLER_REPLACE_TIME - self.moproller_work_time if self.moproller_work_time is not None else None ) @dataclass class MultiMapsListMapInfoBakMaps(RoborockBase): mapflag: Any | None = None add_time: Any | None = None @dataclass class MultiMapsListMapInfo(RoborockBase): _ignore_keys = ["mapFlag"] mapFlag: int name: str add_time: Any | None = None length: Any | None = None bak_maps: list[MultiMapsListMapInfoBakMaps] | None = None @dataclass class MultiMapsList(RoborockBase): _ignore_keys = ["mapFlag"] max_multi_map: int | None = None max_bak_map: int | None = None multi_map_count: int | None = None map_info: list[MultiMapsListMapInfo] | None = None @dataclass class SmartWashParams(RoborockBase): smart_wash: int | None = None wash_interval: int | None = None @dataclass class DustCollectionMode(RoborockBase): mode: RoborockDockDustCollectionModeCode | None = None @dataclass class WashTowelMode(RoborockBase): wash_mode: RoborockDockWashTowelModeCode | None = None @dataclass class NetworkInfo(RoborockBase): ip: str ssid: str | None = None mac: str | None = None bssid: str | None = None rssi: int | None = None @dataclass class DeviceData(RoborockBase): device: HomeDataDevice model: str host: str | None = None @dataclass class RoomMapping(RoborockBase): segment_id: int iot_id: str @dataclass class ChildLockStatus(RoborockBase): lock_status: int @dataclass class FlowLedStatus(RoborockBase): status: int @dataclass class BroadcastMessage(RoborockBase): duid: str ip: str class ServerTimer(NamedTuple): id: str status: str dontknow: int @dataclass class RoborockProductStateValue(RoborockBase): value: list desc: dict @dataclass class RoborockProductState(RoborockBase): dps: int desc: dict value: list[RoborockProductStateValue] @dataclass class RoborockProductSpec(RoborockBase): state: RoborockProductState battery: dict | None = None dry_countdown: dict | None = None extra: dict | None = None offpeak: dict | None = None countdown: dict | None = None mode: dict | None = None ota_nfo: dict | None = None pause: dict | None = None program: dict | None = None shutdown: dict | None = None washing_left: dict | None = None @dataclass class RoborockProduct(RoborockBase): id: int | None = None name: str | None = None model: str | None = None packagename: str | None = None ssid: str | None = None picurl: str | None = None cardpicurl: str | None = None mediumCardpicurl: str | None = None resetwifipicurl: str | None = None configPicUrl: str | None = None pluginPicUrl: str | None = None resetwifitext: dict | None = None tuyaid: str | None = None status: int | None = None rriotid: str | None = None pictures: list | None = None ncMode: str | None = None scope: str | None = None product_tags: list | None = None agreements: list | None = None cardspec: str | None = None plugin_pic_url: str | None = None products_specification: RoborockProductSpec | None = None def __post_init__(self): if self.cardspec: self.products_specification = RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data")) @dataclass class RoborockProductCategory(RoborockBase): id: int display_name: str icon_url: str @dataclass class RoborockCategoryDetail(RoborockBase): category: RoborockProductCategory product_list: list[RoborockProduct] @dataclass class ProductResponse(RoborockBase): category_detail_list: list[RoborockCategoryDetail] @dataclass class DyadProductInfo(RoborockBase): sn: str ssid: str timezone: str posix_timezone: str ip: str mac: str oba: dict @dataclass class DyadSndState(RoborockBase): sid_in_use: int sid_version: int location: str bom: str language: str @dataclass class DyadOtaNfo(RoborockBase): mqttOtaData: dict python-roborock-2.19.0/roborock/exceptions.py000066400000000000000000000041651501065527500213310ustar00rootroot00000000000000"""Roborock exceptions.""" from __future__ import annotations class RoborockException(Exception): """Class for Roborock exceptions.""" class RoborockTimeout(RoborockException): """Class for Roborock timeout exceptions.""" class RoborockConnectionException(RoborockException): """Class for Roborock connection exceptions.""" class RoborockBackoffException(RoborockException): """Class for Roborock exceptions when many retries were made.""" class VacuumError(RoborockException): """Class for vacuum errors.""" class CommandVacuumError(RoborockException): """Class for command vacuum errors.""" def __init__(self, command: str | None, vacuum_error: VacuumError): self.message = f"{command or 'unknown'}: {str(vacuum_error)}" super().__init__(self.message) class UnknownMethodError(RoborockException): """Class for an invalid method being sent.""" class RoborockAccountDoesNotExist(RoborockException): """Class for Roborock account does not exist exceptions.""" class RoborockUrlException(RoborockException): """Class for being unable to get the URL for the Roborock account.""" class RoborockInvalidCode(RoborockException): """Class for Roborock invalid code exceptions.""" class RoborockInvalidEmail(RoborockException): """Class for Roborock invalid formatted email exceptions.""" class RoborockInvalidUserAgreement(RoborockException): """Class for Roborock invalid user agreement exceptions.""" class RoborockNoUserAgreement(RoborockException): """Class for Roborock no user agreement exceptions.""" class RoborockInvalidCredentials(RoborockException): """Class for Roborock credentials have expired or changed.""" class RoborockTooFrequentCodeRequests(RoborockException): """Class for Roborock too frequent code requests exceptions.""" class RoborockMissingParameters(RoborockException): """Class for Roborock missing parameters exceptions.""" class RoborockTooManyRequest(RoborockException): """Class for Roborock too many request exceptions.""" class RoborockRateLimit(RoborockException): """Class for our rate limits exceptions.""" python-roborock-2.19.0/roborock/local_api.py000066400000000000000000000116541501065527500210740ustar00rootroot00000000000000from __future__ import annotations import asyncio import logging from abc import ABC from asyncio import Lock, TimerHandle, Transport, get_running_loop from collections.abc import Callable from dataclasses import dataclass import async_timeout from . import DeviceData from .api import RoborockClient from .exceptions import RoborockConnectionException, RoborockException from .protocol import MessageParser from .roborock_message import RoborockMessage, RoborockMessageProtocol _LOGGER = logging.getLogger(__name__) @dataclass class _LocalProtocol(asyncio.Protocol): """Callbacks for the Roborock local client transport.""" messages_cb: Callable[[bytes], None] connection_lost_cb: Callable[[Exception | None], None] def data_received(self, bytes) -> None: """Called when data is received from the transport.""" self.messages_cb(bytes) def connection_lost(self, exc: Exception | None) -> None: """Called when the transport connection is lost.""" self.connection_lost_cb(exc) class RoborockLocalClient(RoborockClient, ABC): """Roborock local client base class.""" def __init__(self, device_data: DeviceData): """Initialize the Roborock local client.""" if device_data.host is None: raise RoborockException("Host is required") self.host = device_data.host self._batch_structs: list[RoborockMessage] = [] self._executing = False self.remaining = b"" self.transport: Transport | None = None self._mutex = Lock() self.keep_alive_task: TimerHandle | None = None RoborockClient.__init__(self, device_data) self._local_protocol = _LocalProtocol(self._data_received, self._connection_lost) def _data_received(self, message): """Called when data is received from the transport.""" if self.remaining: message = self.remaining + message self.remaining = b"" parser_msg, self.remaining = MessageParser.parse(message, local_key=self.device_info.device.local_key) self.on_message_received(parser_msg) def _connection_lost(self, exc: Exception | None): """Called when the transport connection is lost.""" self._sync_disconnect() self.on_connection_lost(exc) def is_connected(self): return self.transport and self.transport.is_reading() async def keep_alive_func(self, _=None): try: await self.ping() except RoborockException: pass loop = asyncio.get_running_loop() self.keep_alive_task = loop.call_later(10, lambda: asyncio.create_task(self.keep_alive_func())) async def async_connect(self) -> None: should_ping = False async with self._mutex: try: if not self.is_connected(): self._sync_disconnect() async with async_timeout.timeout(self.queue_timeout): self._logger.debug(f"Connecting to {self.host}") loop = get_running_loop() self.transport, _ = await loop.create_connection( # type: ignore lambda: self._local_protocol, self.host, 58867 ) self._logger.info(f"Connected to {self.host}") should_ping = True except BaseException as e: raise RoborockConnectionException(f"Failed connecting to {self.host}") from e if should_ping: await self.hello() await self.keep_alive_func() def _sync_disconnect(self) -> None: loop = asyncio.get_running_loop() if self.transport and loop.is_running(): self._logger.debug(f"Disconnecting from {self.host}") self.transport.close() if self.keep_alive_task: self.keep_alive_task.cancel() async def async_disconnect(self) -> None: async with self._mutex: self._sync_disconnect() async def hello(self): request_id = 1 protocol = RoborockMessageProtocol.HELLO_REQUEST try: return await self.send_message( RoborockMessage( protocol=protocol, seq=request_id, random=22, ) ) except Exception as e: self._logger.error(e) async def ping(self) -> None: request_id = 2 protocol = RoborockMessageProtocol.PING_REQUEST return await self.send_message( RoborockMessage( protocol=protocol, seq=request_id, random=23, ) ) def _send_msg_raw(self, data: bytes): try: if not self.transport: raise RoborockException("Can not send message without connection") self.transport.write(data) except Exception as e: raise RoborockException(e) from e python-roborock-2.19.0/roborock/mqtt/000077500000000000000000000000001501065527500175555ustar00rootroot00000000000000python-roborock-2.19.0/roborock/mqtt/__init__.py000066400000000000000000000003131501065527500216630ustar00rootroot00000000000000"""This module contains the low level MQTT client for the Roborock vacuum cleaner. This is not meant to be used directly, but rather as a base for the higher level modules. """ __all__: list[str] = [] python-roborock-2.19.0/roborock/mqtt/roborock_session.py000066400000000000000000000234011501065527500235120ustar00rootroot00000000000000"""An MQTT session for sending and receiving messages. See create_mqtt_session for a factory function to create an MQTT session. This is a thin wrapper around the async MQTT client that handles dispatching messages from a topic to a callback function, since the async MQTT client does not support this out of the box. It also handles the authentication process and receiving messages from the vacuum cleaner. """ import asyncio import datetime import logging from collections.abc import Callable from contextlib import asynccontextmanager import aiomqtt from aiomqtt import MqttError, TLSParameters from .session import MqttParams, MqttSession, MqttSessionException _LOGGER = logging.getLogger(__name__) _MQTT_LOGGER = logging.getLogger(f"{__name__}.aiomqtt") KEEPALIVE = 60 # Exponential backoff parameters MIN_BACKOFF_INTERVAL = datetime.timedelta(seconds=10) MAX_BACKOFF_INTERVAL = datetime.timedelta(minutes=30) BACKOFF_MULTIPLIER = 1.5 class RoborockMqttSession(MqttSession): """An MQTT session for sending and receiving messages. You can start a session invoking the start() method which will connect to the MQTT broker. A caller may subscribe to a topic, and the session keeps track of which callbacks to invoke for each topic. The client is run as a background task that will run until shutdown. Once connected, the client will wait for messages to be received in a loop. If the connection is lost, the client will be re-created and reconnected. There is backoff to avoid spamming the broker with connection attempts. The client will automatically re-establish any subscriptions when the connection is re-established. """ def __init__(self, params: MqttParams): self._params = params self._background_task: asyncio.Task[None] | None = None self._healthy = False self._backoff = MIN_BACKOFF_INTERVAL self._client: aiomqtt.Client | None = None self._client_lock = asyncio.Lock() self._listeners: dict[str, list[Callable[[bytes], None]]] = {} @property def connected(self) -> bool: """True if the session is connected to the broker.""" return self._healthy async def start(self) -> None: """Start the MQTT session. This has special behavior for the first connection attempt where any failures are raised immediately. This is to allow the caller to handle the failure and retry if desired itself. Once connected, the session will retry connecting in the background. """ start_future: asyncio.Future[None] = asyncio.Future() loop = asyncio.get_event_loop() self._background_task = loop.create_task(self._run_task(start_future)) try: await start_future except MqttError as err: raise MqttSessionException(f"Error starting MQTT session: {err}") from err except Exception as err: raise MqttSessionException(f"Unexpected error starting session: {err}") from err else: _LOGGER.debug("MQTT session started successfully") async def close(self) -> None: """Cancels the MQTT loop and shutdown the client library.""" if self._background_task: self._background_task.cancel() try: await self._background_task except asyncio.CancelledError: pass async with self._client_lock: if self._client: await self._client.close() self._healthy = False async def _run_task(self, start_future: asyncio.Future[None] | None) -> None: """Run the MQTT loop.""" _LOGGER.info("Starting MQTT session") while True: try: async with self._mqtt_client(self._params) as client: # Reset backoff once we've successfully connected self._backoff = MIN_BACKOFF_INTERVAL self._healthy = True if start_future: start_future.set_result(None) start_future = None await self._process_message_loop(client) except MqttError as err: if start_future: _LOGGER.info("MQTT error starting session: %s", err) start_future.set_exception(err) return _LOGGER.info("MQTT error: %s", err) except asyncio.CancelledError as err: if start_future: _LOGGER.debug("MQTT loop was cancelled") start_future.set_exception(err) _LOGGER.debug("MQTT loop was cancelled whiel starting") return # Catch exceptions to avoid crashing the loop # and to allow the loop to retry. except Exception as err: # This error is thrown when the MQTT loop is cancelled # and the generator is not stopped. if "generator didn't stop" in str(err): _LOGGER.debug("MQTT loop was cancelled") return if start_future: _LOGGER.error("Uncaught error starting MQTT session: %s", err) start_future.set_exception(err) return _LOGGER.error("Uncaught error during MQTT session: %s", err) self._healthy = False _LOGGER.info("MQTT session disconnected, retrying in %s seconds", self._backoff.total_seconds()) await asyncio.sleep(self._backoff.total_seconds()) self._backoff = min(self._backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_INTERVAL) @asynccontextmanager async def _mqtt_client(self, params: MqttParams) -> aiomqtt.Client: """Connect to the MQTT broker and listen for messages.""" _LOGGER.debug("Connecting to %s:%s for %s", params.host, params.port, params.username) try: async with aiomqtt.Client( hostname=params.host, port=params.port, username=params.username, password=params.password, keepalive=KEEPALIVE, protocol=aiomqtt.ProtocolVersion.V5, tls_params=TLSParameters() if params.tls else None, timeout=params.timeout, logger=_MQTT_LOGGER, ) as client: _LOGGER.debug("Connected to MQTT broker") # Re-establish any existing subscriptions async with self._client_lock: self._client = client for topic in self._listeners: _LOGGER.debug("Re-establising subscription to topic %s", topic) # TODO: If this fails it will break the whole connection. Make # this retry again in the background with backoff. await client.subscribe(topic) yield client finally: async with self._client_lock: self._client = None async def _process_message_loop(self, client: aiomqtt.Client) -> None: _LOGGER.debug("client=%s", client) _LOGGER.debug("Processing MQTT messages: %s", client.messages) async for message in client.messages: _LOGGER.debug("Received message: %s", message) for listener in self._listeners.get(message.topic.value, []): try: listener(message.payload) except asyncio.CancelledError: raise except Exception as e: _LOGGER.error("Uncaught exception in subscriber callback: %s", e) async def subscribe(self, topic: str, callback: Callable[[bytes], None]) -> Callable[[], None]: """Subscribe to messages on the specified topic and invoke the callback for new messages. The callback will be called with the message payload as a bytes object. The callback should not block since it runs in the async loop. It should not raise any exceptions. The returned callable unsubscribes from the topic when called. """ _LOGGER.debug("Subscribing to topic %s", topic) if topic not in self._listeners: self._listeners[topic] = [] self._listeners[topic].append(callback) async with self._client_lock: if self._client: _LOGGER.debug("Establishing subscription to topic %s", topic) try: await self._client.subscribe(topic) except MqttError as err: raise MqttSessionException(f"Error subscribing to topic: {err}") from err else: _LOGGER.debug("Client not connected, will establish subscription later") return lambda: self._listeners[topic].remove(callback) async def publish(self, topic: str, message: bytes) -> None: """Publish a message on the topic.""" _LOGGER.debug("Sending message to topic %s: %s", topic, message) client: aiomqtt.Client async with self._client_lock: if self._client is None: raise MqttSessionException("Could not publish message, MQTT client not connected") client = self._client try: await client.publish(topic, message) except MqttError as err: raise MqttSessionException(f"Error publishing message: {err}") from err async def create_mqtt_session(params: MqttParams) -> MqttSession: """Create an MQTT session. This function is a factory for creating an MQTT session. This will raise an exception if initial attempt to connect fails. Once connected, the session will retry connecting on failure in the background. """ session = RoborockMqttSession(params) await session.start() return session python-roborock-2.19.0/roborock/mqtt/session.py000066400000000000000000000031541501065527500216150ustar00rootroot00000000000000"""An MQTT session for sending and receiving messages.""" from abc import ABC, abstractmethod from collections.abc import Callable from dataclasses import dataclass from roborock.exceptions import RoborockException DEFAULT_TIMEOUT = 30.0 @dataclass class MqttParams: """MQTT parameters for the connection.""" host: str """MQTT host to connect to.""" port: int """MQTT port to connect to.""" tls: bool """Use TLS for the connection.""" username: str """MQTT username to use for authentication.""" password: str """MQTT password to use for authentication.""" timeout: float = DEFAULT_TIMEOUT """Timeout for communications with the broker in seconds.""" class MqttSession(ABC): """An MQTT session for sending and receiving messages.""" @property @abstractmethod def connected(self) -> bool: """True if the session is connected to the broker.""" @abstractmethod async def subscribe(self, device_id: str, callback: Callable[[bytes], None]) -> Callable[[], None]: """Invoke the callback when messages are received on the topic. The returned callable unsubscribes from the topic when called. """ @abstractmethod async def publish(self, topic: str, message: bytes) -> None: """Publish a message on the specified topic. This will raise an exception if the message could not be sent. """ @abstractmethod async def close(self) -> None: """Cancels the mqtt loop""" class MqttSessionException(RoborockException): """ "Raised when there is an error communicating with MQTT.""" python-roborock-2.19.0/roborock/protocol.py000066400000000000000000000300331501065527500210020ustar00rootroot00000000000000from __future__ import annotations import asyncio import binascii import gzip import hashlib import json import logging from asyncio import BaseTransport, Lock from collections.abc import Callable from construct import ( # type: ignore Bytes, Checksum, ChecksumError, Construct, Container, GreedyBytes, GreedyRange, Int16ub, Int32ub, Optional, Peek, RawCopy, Struct, bytestringtype, stream_seek, stream_tell, ) from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from roborock import BroadcastMessage, RoborockException from roborock.roborock_message import RoborockMessage _LOGGER = logging.getLogger(__name__) SALT = b"TXdfu$jyZ#TZHsg4" A01_HASH = "726f626f726f636b2d67a6d6da" BROADCAST_TOKEN = b"qWKYcdQWrbm9hPqe" AP_CONFIG = 1 SOCK_DISCOVERY = 2 def md5hex(message: str) -> str: md5 = hashlib.md5() md5.update(message.encode()) return md5.hexdigest() class RoborockProtocol(asyncio.DatagramProtocol): def __init__(self, timeout: int = 5): self.timeout = timeout self.transport: BaseTransport | None = None self.devices_found: list[BroadcastMessage] = [] self._mutex = Lock() def __del__(self): self.close() def datagram_received(self, data, _): [broadcast_message], _ = BroadcastParser.parse(data) if broadcast_message.payload: parsed_message = BroadcastMessage.from_dict(json.loads(broadcast_message.payload)) _LOGGER.debug(f"Received broadcast: {parsed_message}") self.devices_found.append(parsed_message) async def discover(self): async with self._mutex: try: loop = asyncio.get_event_loop() self.transport, _ = await loop.create_datagram_endpoint(lambda: self, local_addr=("0.0.0.0", 58866)) await asyncio.sleep(self.timeout) return self.devices_found finally: self.close() self.devices_found = [] def close(self): self.transport.close() if self.transport else None class Utils: """Util class for protocol manipulation.""" @staticmethod def verify_token(token: bytes): """Checks if the given token is of correct type and length.""" if not isinstance(token, bytes): raise TypeError("Token must be bytes") if len(token) != 16: raise ValueError("Wrong token length") @staticmethod def ensure_bytes(msg: bytes | str) -> bytes: if isinstance(msg, str): return msg.encode() return msg @staticmethod def encode_timestamp(_timestamp: int) -> bytes: hex_value = f"{_timestamp:x}".zfill(8) return "".join(list(map(lambda idx: hex_value[idx], [5, 6, 3, 7, 1, 2, 0, 4]))).encode() @staticmethod def md5(data: bytes) -> bytes: """Calculates a md5 hashsum for the given bytes object.""" checksum = hashlib.md5() # nosec checksum.update(data) return checksum.digest() @staticmethod def encrypt_ecb(plaintext: bytes, token: bytes) -> bytes: """Encrypt plaintext with a given token using ecb mode. :param bytes plaintext: Plaintext (json) to encrypt :param bytes token: Token to use :return: Encrypted bytes """ if not isinstance(plaintext, bytes): raise TypeError("plaintext requires bytes") Utils.verify_token(token) cipher = AES.new(token, AES.MODE_ECB) if plaintext: plaintext = pad(plaintext, AES.block_size) return cipher.encrypt(plaintext) return plaintext @staticmethod def decrypt_ecb(ciphertext: bytes, token: bytes) -> bytes: """Decrypt ciphertext with a given token using ecb mode. :param bytes ciphertext: Ciphertext to decrypt :param bytes token: Token to use :return: Decrypted bytes object """ if not isinstance(ciphertext, bytes): raise TypeError("ciphertext requires bytes") if ciphertext: Utils.verify_token(token) aes_key = token decipher = AES.new(aes_key, AES.MODE_ECB) return unpad(decipher.decrypt(ciphertext), AES.block_size) return ciphertext @staticmethod def decrypt_cbc(ciphertext: bytes, token: bytes) -> bytes: """Decrypt ciphertext with a given token using cbc mode. :param bytes ciphertext: Ciphertext to decrypt :param bytes token: Token to use :return: Decrypted bytes object """ if not isinstance(ciphertext, bytes): raise TypeError("ciphertext requires bytes") if ciphertext: Utils.verify_token(token) iv = bytes(AES.block_size) decipher = AES.new(token, AES.MODE_CBC, iv) return unpad(decipher.decrypt(ciphertext), AES.block_size) return ciphertext @staticmethod def crc(data: bytes) -> int: """Gather bytes for checksum calculation.""" return binascii.crc32(data) @staticmethod def decompress(compressed_data: bytes): """Decompress data using gzip.""" return gzip.decompress(compressed_data) class EncryptionAdapter(Construct): """Adapter to handle communication encryption.""" def __init__(self, token_func: Callable): super().__init__() self.token_func = token_func def _parse(self, stream, context, path): subcon1 = Optional(Int16ub) length = subcon1.parse_stream(stream, **context) if not length: if length == 0: subcon1.parse_stream(stream, **context) # seek 2 return None subcon2 = Bytes(length) obj = subcon2.parse_stream(stream, **context) return self._decode(obj, context, path) def _build(self, obj, stream, context, path): if obj is not None: obj2 = self._encode(obj, context, path) subcon1 = Int16ub length = len(obj2) subcon1.build_stream(length, stream, **context) subcon2 = Bytes(length) subcon2.build_stream(obj2, stream, **context) return obj def _encode(self, obj, context, _): """Encrypt the given payload with the token stored in the context. :param obj: JSON object to encrypt """ if context.version == b"A01": iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24] decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8")) f = decipher.encrypt(obj) return f token = self.token_func(context) encrypted = Utils.encrypt_ecb(obj, token) return encrypted def _decode(self, obj, context, _): """Decrypts the given payload with the token stored in the context.""" if context.version == b"A01": iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24] decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8")) f = decipher.decrypt(obj) return f token = self.token_func(context) decrypted = Utils.decrypt_ecb(obj, token) return decrypted class OptionalChecksum(Checksum): def _parse(self, stream, context, path): if not context.message.value.payload: return hash1 = self.checksumfield.parse_stream(stream, **context) hash2 = self.hashfunc(self.bytesfunc(context)) if hash1 != hash2: raise ChecksumError( f"wrong checksum, read {hash1 if not isinstance(hash1, bytestringtype) else binascii.hexlify(hash1)}, " f"computed {hash2 if not isinstance(hash2, bytestringtype) else binascii.hexlify(hash2)}", path=path, ) return hash1 class PrefixedStruct(Struct): def _parse(self, stream, context, path): subcon1 = Peek(Optional(Bytes(3))) peek_version = subcon1.parse_stream(stream, **context) if peek_version not in (b"1.0", b"A01"): subcon2 = Bytes(4) subcon2.parse_stream(stream, **context) return super()._parse(stream, context, path) def _build(self, obj, stream, context, path): prefixed = context.search("prefixed") if not prefixed: return super()._build(obj, stream, context, path) offset = stream_tell(stream, path) stream_seek(stream, offset + 4, 0, path) super()._build(obj, stream, context, path) new_offset = stream_tell(stream, path) subcon1 = Bytes(4) stream_seek(stream, offset, 0, path) subcon1.build_stream(new_offset - offset - subcon1.sizeof(**context), stream, **context) stream_seek(stream, new_offset + 4, 0, path) return obj _Message = RawCopy( Struct( "version" / Bytes(3), "seq" / Int32ub, "random" / Int32ub, "timestamp" / Int32ub, "protocol" / Int16ub, "payload" / EncryptionAdapter( lambda ctx: Utils.md5( Utils.encode_timestamp(ctx.timestamp) + Utils.ensure_bytes(ctx.search("local_key")) + SALT ), ), ) ) _Messages = Struct( "messages" / GreedyRange( PrefixedStruct( "message" / _Message, "checksum" / OptionalChecksum(Optional(Int32ub), Utils.crc, lambda ctx: ctx.message.data), ) ), "remaining" / Optional(GreedyBytes), ) _BroadcastMessage = Struct( "message" / RawCopy( Struct( "version" / Bytes(3), "seq" / Int32ub, "protocol" / Int16ub, "payload" / EncryptionAdapter(lambda ctx: BROADCAST_TOKEN), ) ), "checksum" / Checksum(Int32ub, Utils.crc, lambda ctx: ctx.message.data), ) class _Parser: def __init__(self, con: Construct, required_local_key: bool): self.con = con self.required_local_key = required_local_key def parse(self, data: bytes, local_key: str | None = None) -> tuple[list[RoborockMessage], bytes]: if self.required_local_key and local_key is None: raise RoborockException("Local key is required") parsed = self.con.parse(data, local_key=local_key) parsed_messages = [Container({"message": parsed.message})] if parsed.get("message") else parsed.messages messages = [] for message in parsed_messages: messages.append( RoborockMessage( version=message.message.value.version, seq=message.message.value.seq, random=message.message.value.get("random"), timestamp=message.message.value.get("timestamp"), protocol=message.message.value.protocol, payload=message.message.value.payload, ) ) remaining = parsed.get("remaining") or b"" return messages, remaining def build( self, roborock_messages: list[RoborockMessage] | RoborockMessage, local_key: str, prefixed: bool = True ) -> bytes: if isinstance(roborock_messages, RoborockMessage): roborock_messages = [roborock_messages] messages = [] for roborock_message in roborock_messages: messages.append( { "message": { "value": { "version": roborock_message.version, "seq": roborock_message.seq, "random": roborock_message.random, "timestamp": roborock_message.timestamp, "protocol": roborock_message.protocol, "payload": roborock_message.payload, } }, } ) return self.con.build( {"messages": [message for message in messages], "remaining": b""}, local_key=local_key, prefixed=prefixed ) MessageParser: _Parser = _Parser(_Messages, True) BroadcastParser: _Parser = _Parser(_BroadcastMessage, False) python-roborock-2.19.0/roborock/py.typed000066400000000000000000000000001501065527500202550ustar00rootroot00000000000000python-roborock-2.19.0/roborock/roborock_future.py000066400000000000000000000020241501065527500223520ustar00rootroot00000000000000from __future__ import annotations from asyncio import Future from typing import Any import async_timeout from .exceptions import VacuumError class RoborockFuture: def __init__(self, protocol: int): self.protocol = protocol self.fut: Future = Future() self.loop = self.fut.get_loop() def _set_result(self, item: Any) -> None: if not self.fut.cancelled(): self.fut.set_result(item) def set_result(self, item: Any) -> None: self.loop.call_soon_threadsafe(self._set_result, item) def _set_exception(self, exc: VacuumError) -> None: if not self.fut.cancelled(): self.fut.set_exception(exc) def set_exception(self, exc: VacuumError) -> None: self.loop.call_soon_threadsafe(self._set_exception, exc) async def async_get(self, timeout: float | int) -> tuple[Any, VacuumError | None]: try: async with async_timeout.timeout(timeout): return await self.fut finally: self.fut.cancel() python-roborock-2.19.0/roborock/roborock_message.py000066400000000000000000000127531501065527500224760ustar00rootroot00000000000000from __future__ import annotations import json import math import time from dataclasses import dataclass, field from roborock import RoborockEnum from roborock.util import get_next_int class RoborockMessageProtocol(RoborockEnum): HELLO_REQUEST = 0 HELLO_RESPONSE = 1 PING_REQUEST = 2 PING_RESPONSE = 3 GENERAL_REQUEST = 4 GENERAL_RESPONSE = 5 RPC_REQUEST = 101 RPC_RESPONSE = 102 MAP_RESPONSE = 301 class RoborockDataProtocol(RoborockEnum): ERROR_CODE = 120 STATE = 121 BATTERY = 122 FAN_POWER = 123 WATER_BOX_MODE = 124 MAIN_BRUSH_WORK_TIME = 125 SIDE_BRUSH_WORK_TIME = 126 FILTER_WORK_TIME = 127 ADDITIONAL_PROPS = 128 TASK_COMPLETE = 130 TASK_CANCEL_LOW_POWER = 131 TASK_CANCEL_IN_MOTION = 132 CHARGE_STATUS = 133 DRYING_STATUS = 134 OFFLINE_STATUS = 135 @classmethod def _missing_(cls: type[RoborockEnum], key) -> RoborockEnum: raise ValueError("%s not a valid key for Data Protocol", key) class RoborockDyadDataProtocol(RoborockEnum): DRYING_STATUS = 134 START = 200 STATUS = 201 SELF_CLEAN_MODE = 202 SELF_CLEAN_LEVEL = 203 WARM_LEVEL = 204 CLEAN_MODE = 205 SUCTION = 206 WATER_LEVEL = 207 BRUSH_SPEED = 208 POWER = 209 COUNTDOWN_TIME = 210 AUTO_SELF_CLEAN_SET = 212 AUTO_DRY = 213 MESH_LEFT = 214 BRUSH_LEFT = 215 ERROR = 216 MESH_RESET = 218 BRUSH_RESET = 219 VOLUME_SET = 221 STAND_LOCK_AUTO_RUN = 222 AUTO_SELF_CLEAN_SET_MODE = 223 AUTO_DRY_MODE = 224 SILENT_DRY_DURATION = 225 SILENT_MODE = 226 SILENT_MODE_START_TIME = 227 SILENT_MODE_END_TIME = 228 RECENT_RUN_TIME = 229 TOTAL_RUN_TIME = 230 FEATURE_INFO = 235 RECOVER_SETTINGS = 236 DRY_COUNTDOWN = 237 ID_QUERY = 10000 F_C = 10001 SCHEDULE_TASK = 10002 SND_SWITCH = 10003 SND_STATE = 10004 PRODUCT_INFO = 10005 PRIVACY_INFO = 10006 OTA_NFO = 10007 RPC_REQUEST = 10101 RPC_RESPONSE = 10102 class RoborockZeoProtocol(RoborockEnum): START = 200 # rw PAUSE = 201 # rw SHUTDOWN = 202 # rw STATE = 203 # ro MODE = 204 # rw PROGRAM = 205 # rw CHILD_LOCK = 206 # rw TEMP = 207 # rw RINSE_TIMES = 208 # rw SPIN_LEVEL = 209 # rw DRYING_MODE = 210 # rw DETERGENT_SET = 211 # rw SOFTENER_SET = 212 # rw DETERGENT_TYPE = 213 # rw SOFTENER_TYPE = 214 # rw COUNTDOWN = 217 # rw WASHING_LEFT = 218 # ro DOORLOCK_STATE = 219 # ro ERROR = 220 # ro CUSTOM_PARAM_SAVE = 221 # rw CUSTOM_PARAM_GET = 222 # ro SOUND_SET = 223 # rw TIMES_AFTER_CLEAN = 224 # ro DEFAULT_SETTING = 225 # rw DETERGENT_EMPTY = 226 # ro SOFTENER_EMPTY = 227 # ro LIGHT_SETTING = 229 # rw DETERGENT_VOLUME = 230 # rw SOFTENER_VOLUME = 231 # rw APP_AUTHORIZATION = 232 # rw ID_QUERY = 10000 F_C = 10001 SND_STATE = 10004 PRODUCT_INFO = 10005 PRIVACY_INFO = 10006 OTA_NFO = 10007 WASHING_LOG = 10008 RPC_REQ = 10101 RPC_RESp = 10102 ROBOROCK_DATA_STATUS_PROTOCOL = [ RoborockDataProtocol.ERROR_CODE, RoborockDataProtocol.STATE, RoborockDataProtocol.BATTERY, RoborockDataProtocol.FAN_POWER, RoborockDataProtocol.WATER_BOX_MODE, RoborockDataProtocol.CHARGE_STATUS, ] ROBOROCK_DATA_CONSUMABLE_PROTOCOL = [ RoborockDataProtocol.MAIN_BRUSH_WORK_TIME, RoborockDataProtocol.SIDE_BRUSH_WORK_TIME, RoborockDataProtocol.FILTER_WORK_TIME, ] @dataclass class MessageRetry: method: str retry_id: int @dataclass class RoborockMessage: protocol: RoborockMessageProtocol payload: bytes | None = None seq: int = field(default_factory=lambda: get_next_int(100000, 999999)) version: bytes = b"1.0" random: int = field(default_factory=lambda: get_next_int(10000, 99999)) timestamp: int = field(default_factory=lambda: math.floor(time.time())) message_retry: MessageRetry | None = None def get_request_id(self) -> int | None: if self.payload: payload = json.loads(self.payload.decode()) for data_point_number, data_point in payload.get("dps").items(): if data_point_number in ["101", "102"]: data_point_response = json.loads(data_point) return data_point_response.get("id") return None def get_retry_id(self) -> int | None: if self.message_retry: return self.message_retry.retry_id return self.get_request_id() def get_method(self) -> str | None: if self.message_retry: return self.message_retry.method protocol = self.protocol if self.payload and protocol in [4, 5, 101, 102]: payload = json.loads(self.payload.decode()) for data_point_number, data_point in payload.get("dps").items(): if data_point_number in ["101", "102"]: data_point_response = json.loads(data_point) return data_point_response.get("method") return None def get_params(self) -> list | dict | None: protocol = self.protocol if self.payload and protocol in [4, 101, 102]: payload = json.loads(self.payload.decode()) for data_point_number, data_point in payload.get("dps").items(): if data_point_number in ["101", "102"]: data_point_response = json.loads(data_point) return data_point_response.get("params") return None python-roborock-2.19.0/roborock/roborock_typing.py000066400000000000000000000567051501065527500223710ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from enum import Enum from .containers import ( CleanRecord, CleanSummary, Consumable, DustCollectionMode, RoborockBase, SmartWashParams, Status, WashTowelMode, ) class RoborockCommand(str, Enum): ADD_MOP_TEMPLATE_PARAMS = "add_mop_template_params" APP_AMETHYST_SELF_CHECK = "app_amethyst_self_check" APP_CHARGE = "app_charge" APP_DELETE_WIFI = "app_delete_wifi" APP_GET_AMETHYST_STATUS = "app_get_amethyst_status" APP_GET_CARPET_DEEP_CLEAN_STATUS = "app_get_carpet_deep_clean_status" APP_GET_CLEAN_ESTIMATE_INFO = "app_get_clean_estimate_info" APP_GET_DRYER_SETTING = "app_get_dryer_setting" APP_GET_INIT_STATUS = "app_get_init_status" APP_GET_LOCALE = "app_get_locale" APP_GET_WIFI_LIST = "app_get_wifi_list" APP_GOTO_TARGET = "app_goto_target" APP_KEEP_EASTER_EGG = "app_keep_easter_egg" APP_PAUSE = "app_pause" APP_RC_END = "app_rc_end" APP_RC_MOVE = "app_rc_move" APP_RC_START = "app_rc_start" APP_RC_STOP = "app_rc_stop" APP_RESUME_BUILD_MAP = "app_resume_build_map" APP_RESUME_PATROL = "app_resume_patrol" APP_SEGMENT_CLEAN = "app_segment_clean" APP_SET_AMETHYST_STATUS = "app_set_amethyst_status" APP_SET_CARPET_DEEP_CLEAN_STATUS = "app_set_carpet_deep_clean_status" APP_SET_CROSS_CARPET_CLEANING_STATUS = "app_set_cross_carpet_cleaning_status" APP_SET_DOOR_SILL_BLOCKS = "app_set_door_sill_blocks" APP_SET_DIRTY_REPLENISH_CLEAN_STATUS = "app_set_dirty_replenish_clean_status" APP_SET_DRYER_SETTING = "app_set_dryer_setting" APP_SET_DRYER_STATUS = "app_set_dryer_status" APP_SET_DYNAMIC_CONFIG = "app_set_dynamic_config" APP_SET_IGNORE_STUCK_POINT = "app_set_ignore_stuck_point" APP_SET_SMART_CLIFF_FORBIDDEN = "app_set_smart_cliff_forbidden" APP_SET_SMART_DOOR_SILL = "app_set_smart_door_sill" APP_SPOT = "app_spot" APP_START = "app_start" APP_START_BUILD_MAP = "app_start_build_map" APP_START_COLLECT_DUST = "app_start_collect_dust" APP_START_EASTER_EGG = "app_start_easter_egg" APP_START_PATROL = "app_start_patrol" APP_START_PET_PATROL = "app_start_pet_patrol" APP_START_WASH = "app_start_wash" APP_STAT = "app_stat" APP_STOP = "app_stop" APP_STOP_COLLECT_DUST = "app_stop_collect_dust" APP_STOP_WASH = "app_stop_wash" APP_UPDATE_UNSAVE_MAP = "app_update_unsave_map" APP_WAKEUP_ROBOT = "app_wakeup_robot" APP_ZONED_CLEAN = "app_zoned_clean" CHANGE_SOUND_VOLUME = "change_sound_volume" CHECK_HOMESEC_PASSWORD = "check_homesec_password" CLOSE_DND_TIMER = "close_dnd_timer" CLOSE_VALLEY_ELECTRICITY_TIMER = "close_valley_electricity_timer" DEL_CLEAN_RECORD = "del_clean_record" DEL_CLEAN_RECORD_MAP_V2 = "del_clean_record_map_v2" DEL_MAP = "del_map" DEL_MOP_TEMPLATE_PARAMS = "del_mop_template_params" DEL_SERVER_TIMER = "del_server_timer" DEL_TIMER = "del_timer" DNLD_INSTALL_SOUND = "dnld_install_sound" ENABLE_HOMESEC_VOICE = "enable_homesec_voice" ENABLE_LOG_UPLOAD = "enable_log_upload" END_EDIT_MAP = "end_edit_map" FIND_ME = "find_me" GET_AUTO_DELIVERY_CLEANING_FLUID = "get_auto_delivery_cleaning_fluid" GET_CAMERA_STATUS = "get_camera_status" GET_CARPET_CLEAN_MODE = "get_carpet_clean_mode" GET_CARPET_MODE = "get_carpet_mode" GET_CHILD_LOCK_STATUS = "get_child_lock_status" GET_CLEAN_FOLLOW_GROUND_MATERIAL_STATUS = "get_clean_follow_ground_material_status" GET_CLEAN_MOTOR_MODE = "get_clean_motor_mode" GET_CLEAN_RECORD = "get_clean_record" GET_CLEAN_RECORD_MAP = "get_clean_record_map" GET_CLEAN_SEQUENCE = "get_clean_sequence" GET_CLEAN_SUMMARY = "get_clean_summary" GET_COLLISION_AVOID_STATUS = "get_collision_avoid_status" GET_CONSUMABLE = "get_consumable" GET_CURRENT_SOUND = "get_current_sound" GET_CUSTOM_MODE = "get_custom_mode" GET_CUSTOMIZE_CLEAN_MODE = "get_customize_clean_mode" GET_DEVICE_ICE = "get_device_ice" GET_DEVICE_SDP = "get_device_sdp" GET_DND_TIMER = "get_dnd_timer" GET_DOCK_INFO = "get_dock_info" GET_DUST_COLLECTION_MODE = "get_dust_collection_mode" GET_DUST_COLLECTION_SWITCH_STATUS = "get_dust_collection_switch_status" GET_DYNAMIC_DATA = "get_dynamic_data" GET_DYNAMIC_MAP_DIFF = "get_dynamic_map_diff" GET_FAN_MOTOR_WORK_TIMEOUT = "get_fan_motor_work_timeout" GET_FLOW_LED_STATUS = "get_flow_led_status" GET_FRESH_MAP = "get_fresh_map" GET_FW_FEATURES = "get_fw_features" GET_HOMESEC_CONNECT_STATUS = "get_homesec_connect_status" GET_IDENTIFY_FURNITURE_STATUS = "get_identify_furniture_status" GET_IDENTIFY_GROUND_MATERIAL_STATUS = "get_identify_ground_material_status" GET_LED_STATUS = "get_led_status" GET_LOG_UPLOAD_STATUS = "get_log_upload_status" GET_MAP = "get_map" GET_MAP_BEAUTIFICATION_STATUS = "get_map_beautification_status" GET_MAP_STATUS = "get_map_status" GET_MAP_V1 = "get_map_v1" GET_MAP_V2 = "get_map_v2" GET_MAP_CALIBRATION = "get_map_calibration" # Custom command GET_MOP_MOTOR_STATUS = "get_mop_motor_status" GET_MOP_TEMPLATE_PARAMS_BY_ID = "get_mop_template_params_by_id" GET_MOP_TEMPLATE_PARAMS_SUMMARY = "get_mop_template_params_summary" GET_MULTI_MAP = "get_multi_map" GET_MULTI_MAPS_LIST = "get_multi_maps_list" GET_NETWORK_INFO = "get_network_info" GET_OFFLINE_MAP_STATUS = "get_offline_map_status" GET_PERSIST = "get_persist_map" GET_PROP = "get_prop" GET_RANDOM_PKEY = "get_random_pkey" GET_RECOVER_MAP = "get_recover_map" GET_RECOVER_MAPS = "get_recover_maps" GET_ROOM_MAPPING = "get_room_mapping" GET_SCENES_VALID_TIDS = "get_scenes_valid_tids" GET_SEGMENT_STATUS = "get_segment_status" GET_SERIAL_NUMBER = "get_serial_number" GET_SERVER_TIMER = "get_server_timer" GET_SMART_WASH_PARAMS = "get_smart_wash_params" GET_SOUND_PROGRESS = "get_sound_progress" GET_SOUND_VOLUME = "get_sound_volume" GET_STATUS = "get_status" GET_TESTID = "get_testid" GET_TIMER = "get_timer" GET_TIMER_DETAIL = "get_timer_detail" GET_TIMER_SUMMARY = "get_timer_summary" GET_TIMEZONE = "get_timezone" GET_TURN_SERVER = "get_turn_server" GET_VALLEY_ELECTRICITY_TIMER = "get_valley_electricity_timer" GET_WASH_DEBUG_PARAMS = "get_wash_debug_params" GET_WASH_TOWEL_MODE = "get_wash_towel_mode" GET_WASH_TOWEL_PARAMS = "get_wash_towel_params" GET_WATER_BOX_CUSTOM_MODE = "get_water_box_custom_mode" LOAD_MULTI_MAP = "load_multi_map" MANUAL_BAK_MAP = "manual_bak_map" MANUAL_SEGMENT_MAP = "manual_segment_map" MERGE_SEGMENT = "merge_segment" MOP_MODE = "mop_mode" MOP_TEMPLATE_ID = "mop_template_id" NAME_MULTI_MAP = "name_multi_map" NAME_SEGMENT = "name_segment" PLAY_AUDIO = "play_audio" RECOVER_MAP = "recover_map" RECOVER_MULTI_MAP = "recover_multi_map" RESET_CONSUMABLE = "reset_consumable" RESET_HOMESEC_PASSWORD = "reset_homesec_password" RESET_MAP = "reset_map" RESOLVE_ERROR = "resolve_error" RESUME_SEGMENT_CLEAN = "resume_segment_clean" RESUME_ZONED_CLEAN = "resume_zoned_clean" RETRY_REQUEST = "retry_request" REUNION_SCENES = "reunion_scenes" SAVE_FURNITURES = "save_furnitures" SAVE_MAP = "save_map" SEND_ICE_TO_ROBOT = "send_ice_to_robot" SEND_SDP_TO_ROBOT = "send_sdp_to_robot" SET_AIRDRY_HOURS = "set_airdry_hours" SET_APP_TIMEZONE = "set_app_timezone" SET_AUTO_DELIVERY_CLEANING_FLUID = "set_auto_delivery_cleaning_fluid" SET_CAMERA_STATUS = "set_camera_status" SET_CARPET_AREA = "set_carpet_area" SET_CARPET_CLEAN_MODE = "set_carpet_clean_mode" SET_CARPET_MODE = "set_carpet_mode" SET_CHILD_LOCK_STATUS = "set_child_lock_status" SET_CLEAN_FOLLOW_GROUND_MATERIAL_STATUS = "set_clean_follow_ground_material_status" SET_CLEAN_MOTOR_MODE = "set_clean_motor_mode" SET_CLEAN_SEQUENCE = "set_clean_sequence" SET_CLEAN_REPEAT_TIMES = "set_clean_repeat_times" SET_COLLISION_AVOID_STATUS = "set_collision_avoid_status" SET_CUSTOM_MODE = "set_custom_mode" SET_CUSTOMIZE_CLEAN_MODE = "set_customize_clean_mode" SET_DND_TIMER = "set_dnd_timer" SET_DND_TIMER_ACTIONS = "set_dnd_timer_actions" SET_DUST_COLLECTION_MODE = "set_dust_collection_mode" SET_DUST_COLLECTION_SWITCH_STATUS = "set_dust_collection_switch_status" SET_FAN_MOTOR_WORK_TIMEOUT = "set_fan_motor_work_timeout" SET_FDS_ENDPOINT = "set_fds_endpoint" SET_FLOW_LED_STATUS = "set_flow_led_status" SET_HOMESEC_PASSWORD = "set_homesec_password" SET_IDENTIFY_FURNITURE_STATUS = "set_identify_furniture_status" SET_IDENTIFY_GROUND_MATERIAL_STATUS = "set_identify_ground_material_status" SET_IGNORE_CARPET_ZONE = "set_ignore_carpet_zone" SET_IGNORE_IDENTIFY_AREA = "set_ignore_identify_area" SET_LAB_STATUS = "set_lab_status" SET_LED_STATUS = "set_led_status" SET_MAP_BEAUTIFICATION_STATUS = "set_map_beautification_status" SET_MOP_MODE = "set_mop_mode" SET_MOP_MOTOR_STATUS = "set_mop_motor_status" SET_MOP_TEMPLATE_ID = "set_mop_template_id" SET_OFFLINE_MAP_STATUS = "set_offline_map_status" SET_SCENES_SEGMENTS = "set_scenes_segments" SET_SCENES_ZONES = "set_scenes_zones" SET_SEGMENT_GROUND_MATERIAL = "set_segment_ground_material" SET_SERVER_TIMER = "set_server_timer" SET_SMART_WASH_PARAMS = "set_smart_wash_params" SET_SWITCH_MOP_MODE = "set_switch_map_mode" SET_TIMER = "set_timer" SET_TIMEZONE = "set_timezone" SET_VALLEY_ELECTRICITY_TIMER = "set_valley_electricity_timer" SET_VOICE_CHAT_VOLUME = "set_voice_chat_volume" SET_WASH_DEBUG_PARAMS = "set_wash_debug_params" SET_WASH_TOWEL_MODE = "set_wash_towel_mode" SET_WASH_TOWEL_PARAMS = "set_wash_towel_params" SET_WATER_BOX_CUSTOM_MODE = "set_water_box_custom_mode" SET_WATER_BOX_DISTANCE_OFF = "set_water_box_distance_off" SORT_MOP_TEMPLATE_PARAMS = "sort_mop_template_params" SPLIT_SEGMENT = "split_segment" START_CAMERA_PREVIEW = "start_camera_preview" START_CLEAN = "start_clean" START_EDIT_MAP = "start_edit_map" START_VOICE_CHAT = "start_voice_chat" START_WASH_THEN_CHARGE = "start_wash_then_charge" STOP_CAMERA_PREVIEW = "stop_camera_preview" STOP_FAN_MOTOR_WORK = "stop_fan_motor_work" STOP_GOTO_TARGET = "stop_goto_target" STOP_SEGMENT_CLEAN = "stop_segment_clean" STOP_VOICE_CHAT = "stop_voice_chat" STOP_ZONED_CLEAN = "stop_zoned_clean" SWITCH_VIDEO_QUALITY = "switch_video_quality" SWITCH_WATER_MARK = "switch_water_mark" TEST_SOUND_VOLUME = "test_sound_volume" UPD_SERVER_TIMER = "upd_server_timer" UPD_TIMER = "upd_timer" UPDATE_DOCK = "update_dock" UPDATE_MOP_TEMPLATE_PARAMS = "update_mop_template_params" UPLOAD_DATA_FOR_DEBUG_MODE = "upload_data_for_debug_mode" UPLOAD_PHOTO = "upload_photo" USE_NEW_MAP = "use_new_map" USE_OLD_MAP = "use_old_map" USER_UPLOAD_LOG = "user_upload_log" SET_STRETCH_TAG_STATUS = "set_stretch_tag_status" GET_STRETCH_TAG_STATUS = "get_stretch_tag_status" SET_RIGHT_BRUSH_STRETCH_STATUS = "set_right_brush_stretch_status" GET_RIGHT_BRUSH_STRETCH_STATUS = "get_right_brush_stretch_status" SET_DIRTY_OBJECT_DETECT_STATUS = "set_dirty_object_detect_status" GET_DIRTY_OBJECT_DETECT_STATUS = "get_dirty_object_detect_status" SET_WASH_WATER_TEMPERATURE = "set_wash_water_temperature" GET_WASH_WATER_TEMPERATURE = "get_wash_water_temperature" APP_EMPTY_RINSE_TANK_WATER = "app_empty_rinse_tank_water" SET_PET_SUPPLIES_DEEP_CLEAN_STATUS = "set_pet_supplies_deep_clean_status" GET_PET_SUPPLIES_DEEP_CLEAN_STATUS = "get_pet_supplies_deep_clean_status" SET_AP_MIC_LED_STATUS = "set_ap_mic_led_status" GET_AP_MIC_LED_STATUS = "get_ap_mic_led_status" SET_HANDLE_LEAK_WATER_STATUS = "set_handle_leak_water_status" GET_HANDLE_LEAK_WATER_STATUS = "get_handle_leak_water_status" APP_IGNORE_DIRTY_OBJECTS = "app_ignore_dirty_objects" MATTER_GET_STATUS = "matter.get_status" MATTER_DNLD_KEY = "matter.dnld_key" MATTER_RESET = "matter.reset" SET_GAP_DEEP_CLEAN_STATUS = "set_gap_deep_clean_status" GET_GAP_DEEP_CLEAN_STATUS = "get_gap_deep_clean_status" APP_SET_ROBOT_SETTING = "app_set_robot_setting" APP_GET_ROBOT_SETTING = "app_get_robot_setting" @dataclass class CommandInfo: params: list | dict | int | None = None CommandInfoMap: dict[RoborockCommand | None, CommandInfo] = { RoborockCommand.APP_CHARGE: CommandInfo(params=[]), RoborockCommand.APP_GET_DRYER_SETTING: CommandInfo(params=None), RoborockCommand.APP_GET_INIT_STATUS: CommandInfo(params=[]), RoborockCommand.APP_GOTO_TARGET: CommandInfo(params=[25000, 24850]), RoborockCommand.APP_PAUSE: CommandInfo(params=[]), RoborockCommand.APP_RC_END: CommandInfo(params=[]), RoborockCommand.APP_RC_MOVE: CommandInfo(params=None), RoborockCommand.APP_RC_START: CommandInfo(params=[]), RoborockCommand.APP_RC_STOP: CommandInfo(params=[]), RoborockCommand.APP_SEGMENT_CLEAN: CommandInfo(params=[{"segments": 16, "repeat": 2}]), # RoborockCommand.APP_SEGMENT_CLEAN: CommandInfo(prefix=b"\x00\x00\x00\x87", params=None), RoborockCommand.APP_SET_DRYER_SETTING: CommandInfo(params=None), RoborockCommand.APP_SET_SMART_CLIFF_FORBIDDEN: CommandInfo(params={"zones": [], "map_index": 0}), RoborockCommand.APP_SPOT: CommandInfo(params=[]), RoborockCommand.APP_START: CommandInfo(params=None), # RoborockCommand.APP_START: CommandInfo(prefix=b"\x00\x00\x00\x87", params=[{"use_new_map": 1}]), RoborockCommand.APP_START_COLLECT_DUST: CommandInfo(params=None), RoborockCommand.APP_START_WASH: CommandInfo(params=None), RoborockCommand.APP_STAT: CommandInfo( params=[ { "ver": "0.1", "data": [ { "times": [1682723478], "data": { "region": "America/Sao_Paulo", "pluginVersion": "2820", "mnc": "*", "os": "ios", "osVersion": "16.1", "mcc": "not-cn", "language": "en_BR", "mobileBrand": "*", "appType": "roborock", "mobileModel": "iPhone13,1", }, "type": 2, } ], } ], ), RoborockCommand.APP_STOP: CommandInfo(params=[]), RoborockCommand.APP_STOP_WASH: CommandInfo(params=None), RoborockCommand.APP_WAKEUP_ROBOT: CommandInfo(params=[]), RoborockCommand.APP_ZONED_CLEAN: CommandInfo(params=[[24900, 25100, 26300, 26450, 1]]), RoborockCommand.CHANGE_SOUND_VOLUME: CommandInfo(params=None), RoborockCommand.CLOSE_DND_TIMER: CommandInfo(params=[]), RoborockCommand.CLOSE_VALLEY_ELECTRICITY_TIMER: CommandInfo(params=[]), RoborockCommand.DNLD_INSTALL_SOUND: CommandInfo( params={"url": "https://awsusor0.fds.api.xiaomi.com/app/topazsv/voice-pkg/package/en.pkg", "sid": 3, "sver": 5}, ), RoborockCommand.ENABLE_LOG_UPLOAD: CommandInfo(params=[9, 2]), RoborockCommand.END_EDIT_MAP: CommandInfo(params=[]), RoborockCommand.FIND_ME: CommandInfo(params=None), RoborockCommand.GET_CAMERA_STATUS: CommandInfo(params=[]), RoborockCommand.GET_CARPET_CLEAN_MODE: CommandInfo(params=[]), RoborockCommand.GET_CARPET_MODE: CommandInfo(params=[]), RoborockCommand.GET_CHILD_LOCK_STATUS: CommandInfo(params=[]), RoborockCommand.GET_CLEAN_RECORD: CommandInfo(params=[1682257961]), RoborockCommand.GET_CLEAN_RECORD_MAP: CommandInfo(params={"start_time": 1682597877}), RoborockCommand.GET_CLEAN_SEQUENCE: CommandInfo(params=[]), RoborockCommand.GET_CLEAN_SUMMARY: CommandInfo(params=[]), RoborockCommand.GET_COLLISION_AVOID_STATUS: CommandInfo(params=[]), RoborockCommand.GET_CONSUMABLE: CommandInfo(params=[]), RoborockCommand.GET_CURRENT_SOUND: CommandInfo(params=[]), RoborockCommand.GET_CUSTOMIZE_CLEAN_MODE: CommandInfo(params=[]), RoborockCommand.GET_CUSTOM_MODE: CommandInfo(params=None), RoborockCommand.GET_DEVICE_ICE: CommandInfo(params=[]), RoborockCommand.GET_DEVICE_SDP: CommandInfo(params=[]), RoborockCommand.GET_DND_TIMER: CommandInfo(params=[]), RoborockCommand.GET_DUST_COLLECTION_MODE: CommandInfo(params=None), RoborockCommand.GET_FLOW_LED_STATUS: CommandInfo(params=[]), RoborockCommand.GET_HOMESEC_CONNECT_STATUS: CommandInfo(params=[]), RoborockCommand.GET_IDENTIFY_FURNITURE_STATUS: CommandInfo(params=[]), RoborockCommand.GET_IDENTIFY_GROUND_MATERIAL_STATUS: CommandInfo(params=[]), RoborockCommand.GET_LED_STATUS: CommandInfo(params=[]), RoborockCommand.GET_MAP_V1: CommandInfo(params={}), RoborockCommand.GET_MOP_TEMPLATE_PARAMS_SUMMARY: CommandInfo(params={}), RoborockCommand.GET_MULTI_MAP: CommandInfo(params={"map_index": 0}), RoborockCommand.GET_MULTI_MAPS_LIST: CommandInfo(params=[]), RoborockCommand.GET_NETWORK_INFO: CommandInfo(params=[]), RoborockCommand.GET_PROP: CommandInfo(params=["get_status"]), RoborockCommand.GET_ROOM_MAPPING: CommandInfo(params=[]), RoborockCommand.GET_SCENES_VALID_TIDS: CommandInfo(params={}), RoborockCommand.GET_SERIAL_NUMBER: CommandInfo(params=[]), RoborockCommand.GET_SERVER_TIMER: CommandInfo(params=[]), RoborockCommand.GET_SMART_WASH_PARAMS: CommandInfo(params=None), RoborockCommand.GET_SOUND_PROGRESS: CommandInfo(params=[]), RoborockCommand.GET_SOUND_VOLUME: CommandInfo(params=[]), RoborockCommand.GET_STATUS: CommandInfo(params=None), RoborockCommand.GET_TIMEZONE: CommandInfo(params=[]), RoborockCommand.GET_TURN_SERVER: CommandInfo(params=[]), RoborockCommand.GET_VALLEY_ELECTRICITY_TIMER: CommandInfo(params=[]), RoborockCommand.GET_WASH_TOWEL_MODE: CommandInfo(params=None), RoborockCommand.LOAD_MULTI_MAP: CommandInfo(params=None), RoborockCommand.NAME_SEGMENT: CommandInfo(params=None), RoborockCommand.REUNION_SCENES: CommandInfo(params={"data": [{"tid": "1687830208457"}]}), RoborockCommand.RESET_CONSUMABLE: CommandInfo(params=None), RoborockCommand.RESUME_SEGMENT_CLEAN: CommandInfo(params=None), RoborockCommand.RESUME_ZONED_CLEAN: CommandInfo(params=None), RoborockCommand.RETRY_REQUEST: CommandInfo(params={"retry_id": 439374, "retry_count": 8, "method": "save_map"}), RoborockCommand.SAVE_MAP: CommandInfo( params={ "data": [ [1, 25043, 24952, 26167, 24952], [0, 25043, 25514, 26167, 25514, 26167, 24390, 25043, 24390], [2, 25038, 26782, 26162, 26782, 26162, 25658, 25038, 25658], [100, 0], ], "need_retry": 1, }, ), RoborockCommand.SEND_ICE_TO_ROBOT: CommandInfo( params={ "app_ice": "eyJjYW5kaWRhdGUiOiAiY2FuZGlkYXRlOjE1MzE5NzE5NTEgMSB1ZHAgNDE4MTk5MDMgNTQuMTc0LjE4Ni4yNDkgNTQxNzU" "gdHlwIHJlbGF5IHJhZGRyIDE3Ny4xOC4xMzQuOTkgcnBvcnQgNjQ2OTEgZ2VuZXJhdGlvbiAwIHVmcmFnIDVOMVogbmV0d2" "9yay1pZCAxIG5ldHdvcmstY29zdCAxMCIsICJzZHBNTGluZUluZGV4IjogMSwgInNkcE1pZCI6ICIxIn0=" }, ), RoborockCommand.SET_APP_TIMEZONE: CommandInfo(params=["America/Sao_Paulo", 2]), RoborockCommand.SET_CAMERA_STATUS: CommandInfo(params=[3493]), RoborockCommand.SET_CARPET_CLEAN_MODE: CommandInfo(params={"carpet_clean_mode": 0}), RoborockCommand.SET_CARPET_MODE: CommandInfo( params=[{"enable": 1, "current_high": 500, "current_integral": 450, "current_low": 400, "stall_time": 10}], ), RoborockCommand.SET_CHILD_LOCK_STATUS: CommandInfo(params={"lock_status": 0}), RoborockCommand.SET_CLEAN_MOTOR_MODE: CommandInfo( params=[{"fan_power": 106, "mop_mode": 302, "water_box_mode": 204}] ), RoborockCommand.SET_COLLISION_AVOID_STATUS: CommandInfo(params={"status": 1}), RoborockCommand.SET_CUSTOMIZE_CLEAN_MODE: CommandInfo(params={"data": [], "need_retry": 1}), RoborockCommand.SET_CUSTOM_MODE: CommandInfo(params=[108]), RoborockCommand.SET_DND_TIMER: CommandInfo(params=[22, 0, 8, 0]), RoborockCommand.SET_DUST_COLLECTION_MODE: CommandInfo(params=None), RoborockCommand.SET_FDS_ENDPOINT: CommandInfo(params=["awsusor0.fds.api.xiaomi.com"]), RoborockCommand.SET_FLOW_LED_STATUS: CommandInfo(params={"status": 1}), RoborockCommand.SET_IDENTIFY_FURNITURE_STATUS: CommandInfo(params={"status": 1}), RoborockCommand.SET_IDENTIFY_GROUND_MATERIAL_STATUS: CommandInfo(params={"status": 1}), RoborockCommand.SET_LED_STATUS: CommandInfo(params=[1]), RoborockCommand.SET_MOP_MODE: CommandInfo(params=None), RoborockCommand.SET_SCENES_SEGMENTS: CommandInfo( params={"data": [{"tid": "1687831528786", "segs": [{"sid": 22}, {"sid": 18}]}]} ), RoborockCommand.SET_SCENES_ZONES: CommandInfo( params={"data": [{"zones": [{"zid": 0, "range": [27700, 23750, 30850, 26900]}], "tid": "1687831073722"}]} ), RoborockCommand.SET_SERVER_TIMER: CommandInfo( params={ "data": [["1687793948482", ["39 12 * * 0,1,2,3,4,5,6", ["start_clean", 106, "0", -1]]]], "need_retry": 1, } ), RoborockCommand.SET_SMART_WASH_PARAMS: CommandInfo(params=None), RoborockCommand.SET_TIMEZONE: CommandInfo(params=["America/Sao_Paulo"]), RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER: CommandInfo(params=[0, 0, 8, 0]), RoborockCommand.SET_WASH_TOWEL_MODE: CommandInfo(params=None), RoborockCommand.SET_WATER_BOX_CUSTOM_MODE: CommandInfo(params=[203]), RoborockCommand.START_CAMERA_PREVIEW: CommandInfo(params={"client_id": "443f8636", "quality": "SD"}), RoborockCommand.START_EDIT_MAP: CommandInfo(params=[]), RoborockCommand.START_WASH_THEN_CHARGE: CommandInfo(params=None), RoborockCommand.STOP_CAMERA_PREVIEW: CommandInfo(params={"client_id": "443f8636"}), RoborockCommand.SWITCH_WATER_MARK: CommandInfo(params={"waterMark": "OFF"}), RoborockCommand.TEST_SOUND_VOLUME: CommandInfo(params=None), RoborockCommand.UPD_SERVER_TIMER: CommandInfo(params=[["1687793948482", "off"]]), RoborockCommand.DEL_SERVER_TIMER: CommandInfo(params=["1687793948482"]), } @dataclass class DockSummary(RoborockBase): dust_collection_mode: DustCollectionMode | None = None wash_towel_mode: WashTowelMode | None = None smart_wash_params: SmartWashParams | None = None @dataclass class DeviceProp(RoborockBase): status: Status = field(default_factory=Status) clean_summary: CleanSummary = field(default_factory=CleanSummary) consumable: Consumable = field(default_factory=Consumable) last_clean_record: CleanRecord | None = None dock_summary: DockSummary | None = None dust_collection_mode_name: str | None = None def __post_init__(self) -> None: if ( self.dock_summary and self.dock_summary.dust_collection_mode is not None and self.dock_summary.dust_collection_mode.mode is not None ): self.dust_collection_mode_name = self.dock_summary.dust_collection_mode.mode.name def update(self, device_prop: DeviceProp) -> None: if device_prop.status: self.status = device_prop.status if device_prop.clean_summary: self.clean_summary = device_prop.clean_summary if device_prop.consumable: self.consumable = device_prop.consumable if device_prop.last_clean_record: self.last_clean_record = device_prop.last_clean_record if device_prop.dock_summary: self.dock_summary = device_prop.dock_summary self.__post_init__() python-roborock-2.19.0/roborock/util.py000066400000000000000000000075351501065527500201310ustar00rootroot00000000000000from __future__ import annotations import asyncio import datetime import functools import logging from asyncio import AbstractEventLoop, TimerHandle from collections.abc import Callable, Coroutine, MutableMapping from typing import Any, TypeVar from roborock import RoborockException T = TypeVar("T") DEFAULT_TIME_ZONE: datetime.tzinfo | None = datetime.datetime.now().astimezone().tzinfo def unpack_list(value: list[T], size: int) -> list[T | None]: return (value + [None] * size)[:size] # type: ignore def get_running_loop_or_create_one() -> AbstractEventLoop: try: loop = asyncio.get_event_loop() except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) return loop def parse_datetime_to_roborock_datetime( start_datetime: datetime.datetime, end_datetime: datetime.datetime ) -> tuple[datetime.datetime, datetime.datetime]: now = datetime.datetime.now(DEFAULT_TIME_ZONE) start_datetime = start_datetime.replace( year=now.year, month=now.month, day=now.day, second=0, microsecond=0, tzinfo=DEFAULT_TIME_ZONE ) end_datetime = end_datetime.replace( year=now.year, month=now.month, day=now.day, second=0, microsecond=0, tzinfo=DEFAULT_TIME_ZONE ) if start_datetime > end_datetime: end_datetime += datetime.timedelta(days=1) elif end_datetime < now: start_datetime += datetime.timedelta(days=1) end_datetime += datetime.timedelta(days=1) return start_datetime, end_datetime def parse_time_to_datetime( start_time: datetime.time, end_time: datetime.time ) -> tuple[datetime.datetime, datetime.datetime]: """Help to handle time data.""" start_datetime = datetime.datetime.now(DEFAULT_TIME_ZONE).replace( hour=start_time.hour, minute=start_time.minute, second=0, microsecond=0 ) end_datetime = datetime.datetime.now(DEFAULT_TIME_ZONE).replace( hour=end_time.hour, minute=end_time.minute, second=0, microsecond=0 ) return parse_datetime_to_roborock_datetime(start_datetime, end_datetime) def run_sync(): loop = get_running_loop_or_create_one() def decorator(func): @functools.wraps(func) def wrapped(*args, **kwargs): return loop.run_until_complete(func(*args, **kwargs)) return wrapped return decorator class RepeatableTask: def __init__(self, callback: Callable[[], Coroutine], interval: int): self.callback = callback self.interval = interval self._task: TimerHandle | None = None async def _run_task(self): response = None try: response = await self.callback() except RoborockException: pass loop = asyncio.get_running_loop() self._task = loop.call_later(self.interval, self._run_task_soon) return response def _run_task_soon(self): asyncio.create_task(self._run_task()) def cancel(self): if self._task: self._task.cancel() async def reset(self): self.cancel() return await self._run_task() class RoborockLoggerAdapter(logging.LoggerAdapter): def __init__(self, prefix: str, logger: logging.Logger) -> None: super().__init__(logger, {}) self.prefix = prefix def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]: return f"[{self.prefix}] {msg}", kwargs counter_map: dict[tuple[int, int], int] = {} def get_next_int(min_val: int, max_val: int): """Gets a random int in the range, precached to help keep it fast.""" if (min_val, max_val) not in counter_map: # If we have never seen this range, or if the cache is getting low, make a bunch of preshuffled values. counter_map[(min_val, max_val)] = min_val counter_map[(min_val, max_val)] += 1 return counter_map[(min_val, max_val)] % max_val + min_val python-roborock-2.19.0/roborock/version_1_apis/000077500000000000000000000000001501065527500215115ustar00rootroot00000000000000python-roborock-2.19.0/roborock/version_1_apis/__init__.py000066400000000000000000000002671501065527500236270ustar00rootroot00000000000000from .roborock_client_v1 import AttributeCache, RoborockClientV1 from .roborock_local_client_v1 import RoborockLocalClientV1 from .roborock_mqtt_client_v1 import RoborockMqttClientV1 python-roborock-2.19.0/roborock/version_1_apis/roborock_client_v1.py000066400000000000000000000552351501065527500256610ustar00rootroot00000000000000import asyncio import dataclasses import json import math import struct import time from abc import ABC, abstractmethod from collections.abc import Callable, Coroutine from typing import Any, TypeVar, final from roborock import ( DeviceProp, DockSummary, RoborockCommand, RoborockDockTypeCode, RoborockException, UnknownMethodError, VacuumError, ) from roborock.api import RoborockClient from roborock.command_cache import ( CacheableAttribute, CommandType, RoborockAttribute, find_cacheable_attribute, get_cache_map, ) from roborock.containers import ( ChildLockStatus, CleanRecord, CleanSummary, Consumable, DeviceData, DnDTimer, DustCollectionMode, FlowLedStatus, ModelStatus, MultiMapsList, NetworkInfo, RoborockBase, RoomMapping, S7MaxVStatus, ServerTimer, SmartWashParams, Status, ValleyElectricityTimer, WashTowelMode, ) from roborock.protocol import Utils from roborock.roborock_message import ( ROBOROCK_DATA_CONSUMABLE_PROTOCOL, ROBOROCK_DATA_STATUS_PROTOCOL, RoborockDataProtocol, RoborockMessage, RoborockMessageProtocol, ) from roborock.util import RepeatableTask, get_next_int, unpack_list COMMANDS_SECURED = { RoborockCommand.GET_MAP_V1, RoborockCommand.GET_MULTI_MAP, } CUSTOM_COMMANDS = {RoborockCommand.GET_MAP_CALIBRATION} CLOUD_REQUIRED = COMMANDS_SECURED.union(CUSTOM_COMMANDS) WASH_N_FILL_DOCK = [ RoborockDockTypeCode.empty_wash_fill_dock, RoborockDockTypeCode.s8_dock, RoborockDockTypeCode.p10_dock, RoborockDockTypeCode.p10_pro_dock, RoborockDockTypeCode.s8_maxv_ultra_dock, RoborockDockTypeCode.qrevo_s_dock, RoborockDockTypeCode.saros_r10_dock, RoborockDockTypeCode.qrevo_curv_dock, ] RT = TypeVar("RT", bound=RoborockBase) EVICT_TIME = 60 _SendCommandT = Callable[[RoborockCommand | str, list | dict | int | None], Any] class AttributeCache: def __init__(self, attribute: RoborockAttribute, send_command: _SendCommandT): self.attribute = attribute self._send_command = send_command self.attribute = attribute self.task = RepeatableTask(self._async_value, EVICT_TIME) self._value: Any = None self._mutex = asyncio.Lock() self.unsupported: bool = False @property def value(self): return self._value async def _async_value(self): if self.unsupported: return None try: self._value = await self._send_command(self.attribute.get_command, None) except UnknownMethodError as err: # Limit the amount of times we call unsupported methods self.unsupported = True raise err return self._value async def async_value(self, force: bool = False): async with self._mutex: if self._value is None or force: return await self.task.reset() return self._value def stop(self): self.task.cancel() async def update_value(self, params) -> None: if self.attribute.set_command is None: raise RoborockException(f"{self.attribute.attribute} have no set command") response = await self._send_command(self.attribute.set_command, params) await self._async_value() return response async def add_value(self, params): if self.attribute.add_command is None: raise RoborockException(f"{self.attribute.attribute} have no add command") response = await self._send_command(self.attribute.add_command, params) await self._async_value() return response async def close_value(self, params=None) -> None: if self.attribute.close_command is None: raise RoborockException(f"{self.attribute.attribute} have no close command") response = await self._send_command(self.attribute.close_command, params) await self._async_value() return response async def refresh_value(self): await self._async_value() @dataclasses.dataclass class ListenerModel: protocol_handlers: dict[RoborockDataProtocol, list[Callable[[Status | Consumable], None]]] cache: dict[CacheableAttribute, AttributeCache] class RoborockClientV1(RoborockClient, ABC): """Roborock client base class for version 1 devices.""" _listeners: dict[str, ListenerModel] = {} def __init__(self, device_info: DeviceData, endpoint: str): """Initializes the Roborock client.""" super().__init__(device_info) self._status_type: type[Status] = ModelStatus.get(device_info.model, S7MaxVStatus) self.cache: dict[CacheableAttribute, AttributeCache] = { cacheable_attribute: AttributeCache(attr, self._send_command) for cacheable_attribute, attr in get_cache_map().items() } if device_info.device.duid not in self._listeners: self._listeners[device_info.device.duid] = ListenerModel({}, self.cache) self.listener_model = self._listeners[device_info.device.duid] self._endpoint = endpoint async def async_release(self) -> None: await super().async_release() [item.stop() for item in self.cache.values()] @property def status_type(self) -> type[Status]: """Gets the status type for this device""" return self._status_type async def get_status(self) -> Status: data = self._status_type.from_dict(await self.cache[CacheableAttribute.status].async_value(force=True)) if data is None: return self._status_type() return data async def get_dnd_timer(self) -> DnDTimer | None: return DnDTimer.from_dict(await self.cache[CacheableAttribute.dnd_timer].async_value()) async def get_valley_electricity_timer(self) -> ValleyElectricityTimer | None: return ValleyElectricityTimer.from_dict( await self.cache[CacheableAttribute.valley_electricity_timer].async_value() ) async def get_clean_summary(self) -> CleanSummary | None: clean_summary: dict | list | int = await self.send_command(RoborockCommand.GET_CLEAN_SUMMARY) if isinstance(clean_summary, dict): return CleanSummary.from_dict(clean_summary) elif isinstance(clean_summary, list): clean_time, clean_area, clean_count, records = unpack_list(clean_summary, 4) return CleanSummary( clean_time=clean_time, clean_area=clean_area, clean_count=clean_count, records=records, ) elif isinstance(clean_summary, int): return CleanSummary(clean_time=clean_summary) return None async def get_clean_record(self, record_id: int) -> CleanRecord | None: record: dict | list = await self.send_command(RoborockCommand.GET_CLEAN_RECORD, [record_id]) if isinstance(record, dict): return CleanRecord.from_dict(record) elif isinstance(record, list): if isinstance(record[-1], dict): records = [CleanRecord.from_dict(rec) for rec in record] final_record = records[-1] try: # This code is semi-presumptions - so it is put in a try finally to be safe. final_record.begin = records[0].begin final_record.begin_datetime = records[0].begin_datetime final_record.start_type = records[0].start_type for rec in records[0:-1]: final_record.duration += rec.duration if rec.duration is not None else 0 final_record.area += rec.area if rec.area is not None else 0 final_record.avoid_count += rec.avoid_count if rec.avoid_count is not None else 0 final_record.wash_count += rec.wash_count if rec.wash_count is not None else 0 final_record.square_meter_area += ( rec.square_meter_area if rec.square_meter_area is not None else 0 ) finally: return final_record # There are still a few unknown variables in this. begin, end, duration, area = unpack_list(record, 4) return CleanRecord(begin=begin, end=end, duration=duration, area=area) else: self._logger.warning("Clean record was of a new type, please submit an issue request: %s", record) return None async def get_consumable(self) -> Consumable: data = Consumable.from_dict(await self.cache[CacheableAttribute.consumable].async_value()) if data is None: return Consumable() return data async def get_wash_towel_mode(self) -> WashTowelMode | None: return WashTowelMode.from_dict(await self.cache[CacheableAttribute.wash_towel_mode].async_value()) async def get_dust_collection_mode(self) -> DustCollectionMode | None: return DustCollectionMode.from_dict(await self.cache[CacheableAttribute.dust_collection_mode].async_value()) async def get_smart_wash_params(self) -> SmartWashParams | None: return SmartWashParams.from_dict(await self.cache[CacheableAttribute.smart_wash_params].async_value()) async def get_dock_summary(self, dock_type: RoborockDockTypeCode) -> DockSummary: """Gets the status summary from the dock with the methods available for a given dock. :param dock_type: RoborockDockTypeCode""" commands: list[ Coroutine[ Any, Any, DustCollectionMode | WashTowelMode | SmartWashParams | None, ] ] = [self.get_dust_collection_mode()] if dock_type in WASH_N_FILL_DOCK: commands += [ self.get_wash_towel_mode(), self.get_smart_wash_params(), ] [dust_collection_mode, wash_towel_mode, smart_wash_params] = unpack_list( list(await asyncio.gather(*commands)), 3 ) # type: DustCollectionMode, WashTowelMode | None, SmartWashParams | None # type: ignore return DockSummary(dust_collection_mode, wash_towel_mode, smart_wash_params) async def get_prop(self) -> DeviceProp | None: """Gets device general properties.""" # Mypy thinks that each one of these is typed as a union of all the others. so we do type ignore. status, clean_summary, consumable = await asyncio.gather( *[ self.get_status(), self.get_clean_summary(), self.get_consumable(), ] ) # type: Status, CleanSummary, Consumable # type: ignore last_clean_record = None if clean_summary and clean_summary.records and len(clean_summary.records) > 0: last_clean_record = await self.get_clean_record(clean_summary.records[0]) dock_summary = None if status and status.dock_type is not None and status.dock_type != RoborockDockTypeCode.no_dock: dock_summary = await self.get_dock_summary(status.dock_type) if any([status, clean_summary, consumable]): return DeviceProp( status, clean_summary, consumable, last_clean_record, dock_summary, ) return None async def get_multi_maps_list(self) -> MultiMapsList | None: return await self.send_command(RoborockCommand.GET_MULTI_MAPS_LIST, return_type=MultiMapsList) async def get_networking(self) -> NetworkInfo | None: return await self.send_command(RoborockCommand.GET_NETWORK_INFO, return_type=NetworkInfo) async def get_room_mapping(self) -> list[RoomMapping] | None: """Gets the mapping from segment id -> iot id. Only works on local api.""" mapping: list = await self.send_command(RoborockCommand.GET_ROOM_MAPPING) if isinstance(mapping, list): if len(mapping) == 2 and not isinstance(mapping[0], list): return [RoomMapping(segment_id=mapping[0], iot_id=mapping[1])] return [ RoomMapping(segment_id=segment_id, iot_id=iot_id) # type: ignore for segment_id, iot_id in [unpack_list(room, 2) for room in mapping if isinstance(room, list)] ] return None async def get_child_lock_status(self) -> ChildLockStatus: """Gets current child lock status.""" return ChildLockStatus.from_dict(await self.cache[CacheableAttribute.child_lock_status].async_value()) async def get_flow_led_status(self) -> FlowLedStatus: """Gets current flow led status.""" return FlowLedStatus.from_dict(await self.cache[CacheableAttribute.flow_led_status].async_value()) async def get_sound_volume(self) -> int | None: """Gets current volume level.""" return await self.cache[CacheableAttribute.sound_volume].async_value() async def get_server_timer(self) -> list[ServerTimer]: """Gets current server timer.""" server_timers = await self.cache[CacheableAttribute.server_timer].async_value() if server_timers: if isinstance(server_timers[0], list): return [ServerTimer(*server_timer) for server_timer in server_timers] return [ServerTimer(*server_timers)] return [] async def load_multi_map(self, map_flag: int) -> None: """Load the map into the vacuum's memory.""" await self.send_command(RoborockCommand.LOAD_MULTI_MAP, [map_flag]) def _get_payload( self, method: RoborockCommand | str, params: list | dict | int | None = None, secured=False, ): timestamp = math.floor(time.time()) request_id = get_next_int(10000, 32767) inner = { "id": request_id, "method": method, "params": params or [], } if secured: inner["security"] = { "endpoint": self._endpoint, "nonce": self._nonce.hex().lower(), } payload = bytes( json.dumps( { "dps": {"101": json.dumps(inner, separators=(",", ":"))}, "t": timestamp, }, separators=(",", ":"), ).encode() ) return request_id, timestamp, payload @abstractmethod async def _send_command( self, method: RoborockCommand | str, params: list | dict | int | None = None, ) -> Any: """Send a command to the Roborock device.""" def on_message_received(self, messages: list[RoborockMessage]) -> None: try: self._last_device_msg_in = time.monotonic() for data in messages: protocol = data.protocol if data.payload and protocol in [ RoborockMessageProtocol.RPC_RESPONSE, RoborockMessageProtocol.GENERAL_REQUEST, ]: payload = json.loads(data.payload.decode()) for data_point_number, data_point in payload.get("dps").items(): if data_point_number == "102": data_point_response = json.loads(data_point) request_id = data_point_response.get("id") queue = self._waiting_queue.get(request_id) if queue and queue.protocol == protocol: error = data_point_response.get("error") if error: queue.set_exception( VacuumError( error.get("code"), error.get("message"), ), ) else: result = data_point_response.get("result") if isinstance(result, list) and len(result) == 1: result = result[0] queue.set_result(result) else: self._logger.debug("Received response for unknown request id %s", request_id) else: try: data_protocol = RoborockDataProtocol(int(data_point_number)) self._logger.debug(f"Got device update for {data_protocol.name}: {data_point}") if data_protocol in ROBOROCK_DATA_STATUS_PROTOCOL: if data_protocol not in self.listener_model.protocol_handlers: self._logger.debug( f"Got status update({data_protocol.name}) before get_status was called." ) return value = self.listener_model.cache[CacheableAttribute.status].value value[data_protocol.name] = data_point status = self._status_type.from_dict(value) for listener in self.listener_model.protocol_handlers.get(data_protocol, []): listener(status) elif data_protocol in ROBOROCK_DATA_CONSUMABLE_PROTOCOL: if data_protocol not in self.listener_model.protocol_handlers: self._logger.debug( f"Got consumable update({data_protocol.name})" + "before get_consumable was called." ) return value = self.listener_model.cache[CacheableAttribute.consumable].value value[data_protocol.name] = data_point consumable = Consumable.from_dict(value) for listener in self.listener_model.protocol_handlers.get(data_protocol, []): listener(consumable) elif data_protocol in { RoborockDataProtocol.ADDITIONAL_PROPS, RoborockDataProtocol.DRYING_STATUS, }: # Known data protocol, but not yet sure how to correctly utilize it. return else: self._logger.warning( f"Unknown data protocol {data_point_number}, please create an " f"issue on the python-roborock repository" ) self._logger.info(data) return except ValueError: self._logger.warning( f"Got listener data for {data_point_number}, data: {data_point}. " f"This lets us update data quicker, please open an issue " f"at https://github.com/humbertogontijo/python-roborock/issues" ) pass dps = {data_point_number: data_point} self._logger.debug(f"Got unknown data point {dps}") elif data.payload and protocol == RoborockMessageProtocol.MAP_RESPONSE: payload = data.payload[0:24] [endpoint, _, request_id, _] = struct.unpack("<8s8sH6s", payload) if endpoint.decode().startswith(self._endpoint): try: decrypted = Utils.decrypt_cbc(data.payload[24:], self._nonce) except ValueError as err: raise RoborockException(f"Failed to decode {data.payload!r} for {data.protocol}") from err decompressed = Utils.decompress(decrypted) queue = self._waiting_queue.get(request_id) if queue: if isinstance(decompressed, list): decompressed = decompressed[0] queue.set_result(decompressed) else: self._logger.debug("Received response for unknown request id %s", request_id) else: queue = self._waiting_queue.get(data.seq) if queue: queue.set_result(data.payload) else: self._logger.debug("Received response for unknown request id %s", data.seq) except Exception as ex: self._logger.exception(ex) async def get_from_cache(self, key: CacheableAttribute) -> AttributeCache | None: val = self.cache.get(key) if val is not None: return await val.async_value() return None def add_listener( self, protocol: RoborockDataProtocol, listener: Callable, cache: dict[CacheableAttribute, AttributeCache] ) -> None: self.listener_model.cache = cache if protocol not in self.listener_model.protocol_handlers: self.listener_model.protocol_handlers[protocol] = [] self.listener_model.protocol_handlers[protocol].append(listener) def remove_listener(self, protocol: RoborockDataProtocol, listener: Callable) -> None: self.listener_model.protocol_handlers[protocol].remove(listener) @final async def send_command( self, method: RoborockCommand | str, params: list | dict | int | None = None, return_type: type[RT] | None = None, ) -> RT: cacheable_attribute_result = find_cacheable_attribute(method) cache = None command_type = None if cacheable_attribute_result is not None: cache = self.cache[cacheable_attribute_result.attribute] command_type = cacheable_attribute_result.type response: Any = None if cache is not None and command_type == CommandType.GET: response = await cache.async_value() else: response = await self._send_command(method, params) if cache is not None and command_type == CommandType.CHANGE: await cache.refresh_value() if return_type: return return_type.from_dict(response) return response python-roborock-2.19.0/roborock/version_1_apis/roborock_local_client_v1.py000066400000000000000000000077221501065527500270310ustar00rootroot00000000000000import logging from roborock.local_api import RoborockLocalClient from .. import CommandVacuumError, DeviceData, RoborockCommand, RoborockException from ..exceptions import VacuumError from ..protocol import MessageParser from ..roborock_message import MessageRetry, RoborockMessage, RoborockMessageProtocol from ..util import RoborockLoggerAdapter from .roborock_client_v1 import COMMANDS_SECURED, RoborockClientV1 _LOGGER = logging.getLogger(__name__) class RoborockLocalClientV1(RoborockLocalClient, RoborockClientV1): """Roborock local client for v1 devices.""" def __init__(self, device_data: DeviceData, queue_timeout: int = 4): """Initialize the Roborock local client.""" RoborockLocalClient.__init__(self, device_data) RoborockClientV1.__init__(self, device_data, "abc") self.queue_timeout = queue_timeout self._logger = RoborockLoggerAdapter(device_data.device.name, _LOGGER) def build_roborock_message( self, method: RoborockCommand | str, params: list | dict | int | None = None ) -> RoborockMessage: secured = True if method in COMMANDS_SECURED else False request_id, timestamp, payload = self._get_payload(method, params, secured) self._logger.debug("Building message id %s for method %s", request_id, method) request_protocol = RoborockMessageProtocol.GENERAL_REQUEST message_retry: MessageRetry | None = None if method == RoborockCommand.RETRY_REQUEST and isinstance(params, dict): message_retry = MessageRetry(method=params["method"], retry_id=params["retry_id"]) return RoborockMessage( timestamp=timestamp, protocol=request_protocol, payload=payload, message_retry=message_retry ) async def _send_command( self, method: RoborockCommand | str, params: list | dict | int | None = None, ): roborock_message = self.build_roborock_message(method, params) return await self.send_message(roborock_message) async def send_message(self, roborock_message: RoborockMessage): await self.validate_connection() method = roborock_message.get_method() params = roborock_message.get_params() request_id: int | None if not method or not method.startswith("get"): request_id = roborock_message.seq response_protocol = request_id + 1 else: request_id = roborock_message.get_request_id() response_protocol = RoborockMessageProtocol.GENERAL_REQUEST if request_id is None: raise RoborockException(f"Failed build message {roborock_message}") local_key = self.device_info.device.local_key msg = MessageParser.build(roborock_message, local_key=local_key) if method: self._logger.debug(f"id={request_id} Requesting method {method} with {params}") # Send the command to the Roborock device async_response = self._async_response(request_id, response_protocol) self._send_msg_raw(msg) diagnostic_key = method if method is not None else "unknown" try: response = await async_response except VacuumError as err: self._diagnostic_data[diagnostic_key] = { "params": roborock_message.get_params(), "error": err, } raise CommandVacuumError(method, err) from err self._diagnostic_data[diagnostic_key] = { "params": roborock_message.get_params(), "response": response, } if roborock_message.protocol == RoborockMessageProtocol.GENERAL_REQUEST: self._logger.debug(f"id={request_id} Response from method {roborock_message.get_method()}: {response}") if response == "retry": retry_id = roborock_message.get_retry_id() return self.send_command( RoborockCommand.RETRY_REQUEST, {"retry_id": retry_id, "retry_count": 8, "method": method} ) return response python-roborock-2.19.0/roborock/version_1_apis/roborock_mqtt_client_v1.py000066400000000000000000000104611501065527500267160ustar00rootroot00000000000000import base64 import logging from vacuum_map_parser_base.config.color import ColorsPalette from vacuum_map_parser_base.config.image_config import ImageConfig from vacuum_map_parser_base.config.size import Sizes from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser from roborock.cloud_api import RoborockMqttClient from ..containers import DeviceData, UserData from ..exceptions import CommandVacuumError, RoborockException, VacuumError from ..protocol import MessageParser, Utils from ..roborock_message import ( RoborockMessage, RoborockMessageProtocol, ) from ..roborock_typing import RoborockCommand from ..util import RoborockLoggerAdapter from .roborock_client_v1 import COMMANDS_SECURED, CUSTOM_COMMANDS, RoborockClientV1 _LOGGER = logging.getLogger(__name__) class RoborockMqttClientV1(RoborockMqttClient, RoborockClientV1): """Roborock mqtt client for v1 devices.""" def __init__(self, user_data: UserData, device_info: DeviceData, queue_timeout: int = 10) -> None: """Initialize the Roborock mqtt client.""" rriot = user_data.rriot if rriot is None: raise RoborockException("Got no rriot data from user_data") endpoint = base64.b64encode(Utils.md5(rriot.k.encode())[8:14]).decode() RoborockMqttClient.__init__(self, user_data, device_info) RoborockClientV1.__init__(self, device_info, endpoint) self.queue_timeout = queue_timeout self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER) async def send_message(self, roborock_message: RoborockMessage): await self.validate_connection() method = roborock_message.get_method() params = roborock_message.get_params() request_id = roborock_message.get_request_id() if request_id is None: raise RoborockException(f"Failed build message {roborock_message}") response_protocol = ( RoborockMessageProtocol.MAP_RESPONSE if method in COMMANDS_SECURED else RoborockMessageProtocol.RPC_RESPONSE ) local_key = self.device_info.device.local_key msg = MessageParser.build(roborock_message, local_key, False) self._logger.debug(f"id={request_id} Requesting method {method} with {params}") async_response = self._async_response(request_id, response_protocol) self._send_msg_raw(msg) diagnostic_key = method if method is not None else "unknown" try: response = await async_response except VacuumError as err: self._diagnostic_data[diagnostic_key] = { "params": roborock_message.get_params(), "error": err, } raise CommandVacuumError(method, err) from err self._diagnostic_data[diagnostic_key] = { "params": roborock_message.get_params(), "response": response, } if response_protocol == RoborockMessageProtocol.MAP_RESPONSE: self._logger.debug(f"id={request_id} Response from {method}: {len(response)} bytes") else: self._logger.debug(f"id={request_id} Response from {method}: {response}") return response async def _send_command( self, method: RoborockCommand | str, params: list | dict | int | None = None, ): if method in CUSTOM_COMMANDS: # When we have more custom commands do something more complicated here return await self._get_calibration_points() request_id, timestamp, payload = self._get_payload(method, params, True) self._logger.debug("Building message id %s for method %s", request_id, method) request_protocol = RoborockMessageProtocol.RPC_REQUEST roborock_message = RoborockMessage(timestamp=timestamp, protocol=request_protocol, payload=payload) return await self.send_message(roborock_message) async def _get_calibration_points(self): map: bytes = await self.send_command(RoborockCommand.GET_MAP_V1) parser = RoborockMapDataParser(ColorsPalette(), Sizes(), [], ImageConfig(), []) parsed_map = parser.parse(map) calibration = parsed_map.calibration() self._logger.info(parsed_map.calibration()) return calibration async def get_map_v1(self) -> bytes | None: return await self.send_command(RoborockCommand.GET_MAP_V1) python-roborock-2.19.0/roborock/version_a01_apis/000077500000000000000000000000001501065527500217325ustar00rootroot00000000000000python-roborock-2.19.0/roborock/version_a01_apis/__init__.py000066400000000000000000000001571501065527500240460ustar00rootroot00000000000000from .roborock_client_a01 import RoborockClientA01 from .roborock_mqtt_client_a01 import RoborockMqttClientA01 python-roborock-2.19.0/roborock/version_a01_apis/roborock_client_a01.py000066400000000000000000000170621501065527500261310ustar00rootroot00000000000000import dataclasses import json import logging import typing from abc import ABC, abstractmethod from collections.abc import Callable from datetime import time from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from roborock import DeviceData from roborock.api import RoborockClient from roborock.code_mappings import ( DyadBrushSpeed, DyadCleanMode, DyadError, DyadSelfCleanLevel, DyadSelfCleanMode, DyadSuction, DyadWarmLevel, DyadWaterLevel, RoborockDyadStateCode, ZeoDetergentType, ZeoDryingMode, ZeoError, ZeoMode, ZeoProgram, ZeoRinse, ZeoSoftenerType, ZeoSpin, ZeoState, ZeoTemperature, ) from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory from roborock.roborock_message import ( RoborockDyadDataProtocol, RoborockMessage, RoborockMessageProtocol, RoborockZeoProtocol, ) _LOGGER = logging.getLogger(__name__) @dataclasses.dataclass class A01ProtocolCacheEntry: post_process_fn: Callable value: typing.Any | None = None # Right now this cache is not active, it was too much complexity for the initial addition of dyad. protocol_entries = { RoborockDyadDataProtocol.STATUS: A01ProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name), RoborockDyadDataProtocol.SELF_CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name), RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: A01ProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name), RoborockDyadDataProtocol.WARM_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWarmLevel(val).name), RoborockDyadDataProtocol.CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadCleanMode(val).name), RoborockDyadDataProtocol.SUCTION: A01ProtocolCacheEntry(lambda val: DyadSuction(val).name), RoborockDyadDataProtocol.WATER_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWaterLevel(val).name), RoborockDyadDataProtocol.BRUSH_SPEED: A01ProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name), RoborockDyadDataProtocol.POWER: A01ProtocolCacheEntry(lambda val: int(val)), RoborockDyadDataProtocol.AUTO_DRY: A01ProtocolCacheEntry(lambda val: bool(val)), RoborockDyadDataProtocol.MESH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)), RoborockDyadDataProtocol.BRUSH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)), RoborockDyadDataProtocol.ERROR: A01ProtocolCacheEntry(lambda val: DyadError(val).name), RoborockDyadDataProtocol.VOLUME_SET: A01ProtocolCacheEntry(lambda val: int(val)), RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: A01ProtocolCacheEntry(lambda val: bool(val)), RoborockDyadDataProtocol.AUTO_DRY_MODE: A01ProtocolCacheEntry(lambda val: bool(val)), RoborockDyadDataProtocol.SILENT_DRY_DURATION: A01ProtocolCacheEntry(lambda val: int(val)), # in minutes RoborockDyadDataProtocol.SILENT_MODE: A01ProtocolCacheEntry(lambda val: bool(val)), RoborockDyadDataProtocol.SILENT_MODE_START_TIME: A01ProtocolCacheEntry( lambda val: time(hour=int(val / 60), minute=val % 60) ), # in minutes since 00:00 RoborockDyadDataProtocol.SILENT_MODE_END_TIME: A01ProtocolCacheEntry( lambda val: time(hour=int(val / 60), minute=val % 60) ), # in minutes since 00:00 RoborockDyadDataProtocol.RECENT_RUN_TIME: A01ProtocolCacheEntry( lambda val: [int(v) for v in val.split(",")] ), # minutes of cleaning in past few days. RoborockDyadDataProtocol.TOTAL_RUN_TIME: A01ProtocolCacheEntry(lambda val: int(val)), RoborockDyadDataProtocol.SND_STATE: A01ProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)), RoborockDyadDataProtocol.PRODUCT_INFO: A01ProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)), } zeo_data_protocol_entries = { # ro RoborockZeoProtocol.STATE: A01ProtocolCacheEntry(lambda val: ZeoState(val).name), RoborockZeoProtocol.COUNTDOWN: A01ProtocolCacheEntry(lambda val: int(val)), RoborockZeoProtocol.WASHING_LEFT: A01ProtocolCacheEntry(lambda val: int(val)), RoborockZeoProtocol.ERROR: A01ProtocolCacheEntry(lambda val: ZeoError(val).name), RoborockZeoProtocol.TIMES_AFTER_CLEAN: A01ProtocolCacheEntry(lambda val: int(val)), RoborockZeoProtocol.DETERGENT_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)), RoborockZeoProtocol.SOFTENER_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)), # rw RoborockZeoProtocol.MODE: A01ProtocolCacheEntry(lambda val: ZeoMode(val).name), RoborockZeoProtocol.PROGRAM: A01ProtocolCacheEntry(lambda val: ZeoProgram(val).name), RoborockZeoProtocol.TEMP: A01ProtocolCacheEntry(lambda val: ZeoTemperature(val).name), RoborockZeoProtocol.RINSE_TIMES: A01ProtocolCacheEntry(lambda val: ZeoRinse(val).name), RoborockZeoProtocol.SPIN_LEVEL: A01ProtocolCacheEntry(lambda val: ZeoSpin(val).name), RoborockZeoProtocol.DRYING_MODE: A01ProtocolCacheEntry(lambda val: ZeoDryingMode(val).name), RoborockZeoProtocol.DETERGENT_TYPE: A01ProtocolCacheEntry(lambda val: ZeoDetergentType(val).name), RoborockZeoProtocol.SOFTENER_TYPE: A01ProtocolCacheEntry(lambda val: ZeoSoftenerType(val).name), RoborockZeoProtocol.SOUND_SET: A01ProtocolCacheEntry(lambda val: bool(val)), } class RoborockClientA01(RoborockClient, ABC): """Roborock client base class for A01 devices.""" def __init__(self, device_info: DeviceData, category: RoborockCategory): """Initialize the Roborock client.""" super().__init__(device_info) self.category = category def on_message_received(self, messages: list[RoborockMessage]) -> None: for message in messages: protocol = message.protocol if message.payload and protocol in [ RoborockMessageProtocol.RPC_RESPONSE, RoborockMessageProtocol.GENERAL_REQUEST, ]: payload = message.payload try: payload = unpad(payload, AES.block_size) except Exception as err: self._logger.debug("Failed to unpad payload: %s", err) continue payload_json = json.loads(payload.decode()) for data_point_number, data_point in payload_json.get("dps").items(): data_point_protocol: RoborockDyadDataProtocol | RoborockZeoProtocol self._logger.debug("received msg with dps, protocol: %s, %s", data_point_number, protocol) entries: dict if self.category == RoborockCategory.WET_DRY_VAC: data_point_protocol = RoborockDyadDataProtocol(int(data_point_number)) entries = protocol_entries elif self.category == RoborockCategory.WASHING_MACHINE: data_point_protocol = RoborockZeoProtocol(int(data_point_number)) entries = zeo_data_protocol_entries else: continue if data_point_protocol in entries: # Auto convert into data struct we want. converted_response = entries[data_point_protocol].post_process_fn(data_point) queue = self._waiting_queue.get(int(data_point_number)) if queue and queue.protocol == protocol: queue.set_result(converted_response) @abstractmethod async def update_values( self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol] ) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, typing.Any]: """This should handle updating for each given protocol.""" python-roborock-2.19.0/roborock/version_a01_apis/roborock_mqtt_client_a01.py000066400000000000000000000061711501065527500271750ustar00rootroot00000000000000import asyncio import json import logging import typing from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from roborock.cloud_api import RoborockMqttClient from roborock.containers import DeviceData, RoborockCategory, UserData from roborock.exceptions import RoborockException from roborock.protocol import MessageParser from roborock.roborock_message import ( RoborockDyadDataProtocol, RoborockMessage, RoborockMessageProtocol, RoborockZeoProtocol, ) from ..util import RoborockLoggerAdapter from .roborock_client_a01 import RoborockClientA01 _LOGGER = logging.getLogger(__name__) class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01): """Roborock mqtt client for A01 devices.""" def __init__( self, user_data: UserData, device_info: DeviceData, category: RoborockCategory, queue_timeout: int = 10 ) -> None: """Initialize the Roborock mqtt client.""" rriot = user_data.rriot if rriot is None: raise RoborockException("Got no rriot data from user_data") RoborockMqttClient.__init__(self, user_data, device_info) RoborockClientA01.__init__(self, device_info, category) self.queue_timeout = queue_timeout self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER) async def send_message(self, roborock_message: RoborockMessage): await self.validate_connection() response_protocol = RoborockMessageProtocol.RPC_RESPONSE local_key = self.device_info.device.local_key m = MessageParser.build(roborock_message, local_key, prefixed=False) # self._logger.debug(f"id={request_id} Requesting method {method} with {params}") payload = json.loads(unpad(roborock_message.payload, AES.block_size)) futures = [] if "10000" in payload["dps"]: for dps in json.loads(payload["dps"]["10000"]): futures.append(self._async_response(dps, response_protocol)) self._send_msg_raw(m) responses = await asyncio.gather(*futures, return_exceptions=True) dps_responses: dict[int, typing.Any] = {} if "10000" in payload["dps"]: for i, dps in enumerate(json.loads(payload["dps"]["10000"])): response = responses[i] if isinstance(response, BaseException): self._logger.warning("Timed out get req for %s after %s s", dps, self.queue_timeout) dps_responses[dps] = None else: dps_responses[dps] = response return dps_responses async def update_values( self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol] ) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, typing.Any]: payload = {"dps": {RoborockDyadDataProtocol.ID_QUERY: str([int(protocol) for protocol in dyad_data_protocols])}} return await self.send_message( RoborockMessage( protocol=RoborockMessageProtocol.RPC_REQUEST, version=b"A01", payload=pad(json.dumps(payload).encode("utf-8"), AES.block_size), ) ) python-roborock-2.19.0/roborock/web_api.py000066400000000000000000000570511501065527500205600ustar00rootroot00000000000000from __future__ import annotations import base64 import hashlib import hmac import logging import math import secrets import time import aiohttp from aiohttp import ContentTypeError, FormData from pyrate_limiter import BucketFullException, Duration, Limiter, Rate from roborock.containers import HomeData, HomeDataRoom, HomeDataScene, ProductResponse, RRiot, UserData from roborock.exceptions import ( RoborockAccountDoesNotExist, RoborockException, RoborockInvalidCode, RoborockInvalidCredentials, RoborockInvalidEmail, RoborockInvalidUserAgreement, RoborockMissingParameters, RoborockNoUserAgreement, RoborockRateLimit, RoborockTooFrequentCodeRequests, RoborockTooManyRequest, RoborockUrlException, ) _LOGGER = logging.getLogger(__name__) class RoborockApiClient: _LOGIN_RATES = [ Rate(1, Duration.SECOND), Rate(3, Duration.MINUTE), Rate(10, Duration.HOUR), Rate(20, Duration.DAY), ] _HOME_DATA_RATES = [ Rate(1, Duration.SECOND), Rate(5, Duration.MINUTE), Rate(15, Duration.HOUR), Rate(40, Duration.DAY), ] _login_limiter = Limiter(_LOGIN_RATES) _home_data_limiter = Limiter(_HOME_DATA_RATES) def __init__(self, username: str, base_url=None, session: aiohttp.ClientSession | None = None) -> None: """Sample API Client.""" self._username = username self._default_url = "https://euiot.roborock.com" self.base_url = base_url self._device_identifier = secrets.token_urlsafe(16) self.session = session async def _get_base_url(self) -> str: if not self.base_url: url_request = PreparedRequest(self._default_url, self.session) response = await url_request.request( "post", "/api/v1/getUrlByEmail", params={"email": self._username, "needtwostepauth": "false"}, ) if response is None: raise RoborockUrlException("get url by email returned None") response_code = response.get("code") if response_code != 200: _LOGGER.info("Get base url failed for %s with the following context: %s", self._username, response) if response_code == 2003: raise RoborockInvalidEmail("Your email was incorrectly formatted.") elif response_code == 1001: raise RoborockMissingParameters( "You are missing parameters for this request, are you sure you entered your username?" ) elif response_code == 9002: raise RoborockTooManyRequest("Please temporarily disable making requests and try again later.") raise RoborockUrlException(f"error code: {response_code} msg: {response.get('error')}") response_data = response.get("data") if response_data is None: raise RoborockUrlException("response does not have 'data'") self.base_url = response_data.get("url") return self.base_url def _get_header_client_id(self): md5 = hashlib.md5() md5.update(self._username.encode()) md5.update(self._device_identifier.encode()) return base64.b64encode(md5.digest()).decode() def _process_extra_hawk_values(self, values: dict | None) -> str: if values is None: return "" else: sorted_keys = sorted(values.keys()) result = [] for key in sorted_keys: value = values.get(key) result.append(f"{key}={value}") return hashlib.md5("&".join(result).encode()).hexdigest() def _get_hawk_authentication( self, rriot: RRiot, url: str, formdata: dict | None = None, params: dict | None = None ) -> str: timestamp = math.floor(time.time()) nonce = secrets.token_urlsafe(6) formdata_str = self._process_extra_hawk_values(formdata) params_str = self._process_extra_hawk_values(params) prestr = ":".join( [ rriot.u, rriot.s, nonce, str(timestamp), hashlib.md5(url.encode()).hexdigest(), params_str, formdata_str, ] ) mac = base64.b64encode(hmac.new(rriot.h.encode(), prestr.encode(), hashlib.sha256).digest()).decode() return f'Hawk id="{rriot.u}",s="{rriot.s}",ts="{timestamp}",nonce="{nonce}",mac="{mac}"' async def nc_prepare(self, user_data: UserData, timezone: str) -> dict: """This gets a few critical parameters for adding a device to your account.""" if ( user_data.rriot is None or user_data.rriot.r is None or user_data.rriot.u is None or user_data.rriot.r.a is None ): raise RoborockException("Your userdata is missing critical attributes.") base_url = user_data.rriot.r.a prepare_request = PreparedRequest(base_url, self.session) hid = await self._get_home_id(user_data) data = FormData() data.add_field("hid", hid) data.add_field("tzid", timezone) prepare_response = await prepare_request.request( "post", "/nc/prepare", headers={ "Authorization": self._get_hawk_authentication( user_data.rriot, "/nc/prepare", {"hid": hid, "tzid": timezone} ), }, data=data, ) if prepare_response is None: raise RoborockException("prepare_response is None") if not prepare_response.get("success"): raise RoborockException(f"{prepare_response.get('msg')} - response code: {prepare_response.get('code')}") return prepare_response["result"] async def add_device(self, user_data: UserData, s: str, t: str) -> dict: """This will add a new device to your account it is recommended to only use this during a pairing cycle with a device. Please see here: https://github.com/Python-roborock/Roborockmitmproxy/blob/main/handshake_protocol.md """ if ( user_data.rriot is None or user_data.rriot.r is None or user_data.rriot.u is None or user_data.rriot.r.a is None ): raise RoborockException("Your userdata is missing critical attributes.") base_url = user_data.rriot.r.a add_device_request = PreparedRequest(base_url, self.session) add_device_response = await add_device_request.request( "GET", "/user/devices/newadd", headers={ "Authorization": self._get_hawk_authentication( user_data.rriot, "/user/devices/newadd", params={"s": s, "t": t} ), }, params={"s": s, "t": t}, ) if add_device_response is None: raise RoborockException("add_device is None") if not add_device_response.get("success"): raise RoborockException( f"{add_device_response.get('msg')} - response code: {add_device_response.get('code')}" ) return add_device_response["result"] async def request_code(self) -> None: try: self._login_limiter.try_acquire("login") except BucketFullException as ex: _LOGGER.info(ex.meta_info) raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex base_url = await self._get_base_url() header_clientid = self._get_header_client_id() code_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) code_response = await code_request.request( "post", "/api/v1/sendEmailCode", params={ "username": self._username, "type": "auth", }, ) if code_response is None: raise RoborockException("Failed to get a response from send email code") response_code = code_response.get("code") if response_code != 200: _LOGGER.info("Request code failed for %s with the following context: %s", self._username, code_response) if response_code == 2008: raise RoborockAccountDoesNotExist("Account does not exist - check your login and try again.") elif response_code == 9002: raise RoborockTooFrequentCodeRequests("You have attempted to request too many codes. Try again later") else: raise RoborockException(f"{code_response.get('msg')} - response code: {code_response.get('code')}") async def pass_login(self, password: str) -> UserData: try: self._login_limiter.try_acquire("login") except BucketFullException as ex: _LOGGER.info(ex.meta_info) raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex base_url = await self._get_base_url() header_clientid = self._get_header_client_id() login_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) login_response = await login_request.request( "post", "/api/v1/login", params={ "username": self._username, "password": password, "needtwostepauth": "false", }, ) if login_response is None: raise RoborockException("Login response is none") if login_response.get("code") != 200: _LOGGER.info("Login failed for %s with the following context: %s", self._username, login_response) raise RoborockException(f"{login_response.get('msg')} - response code: {login_response.get('code')}") user_data = login_response.get("data") if not isinstance(user_data, dict): raise RoborockException("Got unexpected data type for user_data") return UserData.from_dict(user_data) async def pass_login_v3(self, password: str) -> UserData: """Seemingly it follows the format below, but password is encrypted in some manner. # login_response = await login_request.request( # "post", # "/api/v3/auth/email/login", # params={ # "email": self._username, # "password": password, # "twoStep": 1, # "version": 0 # }, # ) """ raise NotImplementedError("Pass_login_v3 has not yet been implemented") async def code_login(self, code: int | str) -> UserData: base_url = await self._get_base_url() header_clientid = self._get_header_client_id() login_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) login_response = await login_request.request( "post", "/api/v1/loginWithCode", params={ "username": self._username, "verifycode": code, "verifycodetype": "AUTH_EMAIL_CODE", }, ) if login_response is None: raise RoborockException("Login request response is None") response_code = login_response.get("code") if response_code != 200: _LOGGER.info("Login failed for %s with the following context: %s", self._username, login_response) if response_code == 2018: raise RoborockInvalidCode("Invalid code - check your code and try again.") if response_code == 3009: raise RoborockNoUserAgreement("You must accept the user agreement in the Roborock app to continue.") if response_code == 3006: raise RoborockInvalidUserAgreement( "User agreement must be accepted again - or you are attempting to use the Mi Home app account." ) raise RoborockException(f"{login_response.get('msg')} - response code: {response_code}") user_data = login_response.get("data") if not isinstance(user_data, dict): raise RoborockException("Got unexpected data type for user_data") return UserData.from_dict(user_data) async def _get_home_id(self, user_data: UserData): base_url = await self._get_base_url() header_clientid = self._get_header_client_id() home_id_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) home_id_response = await home_id_request.request( "get", "/api/v1/getHomeDetail", headers={"Authorization": user_data.token}, ) if home_id_response is None: raise RoborockException("home_id_response is None") if home_id_response.get("code") != 200: _LOGGER.info("Get Home Id failed with the following context: %s", home_id_response) if home_id_response.get("code") == 2010: raise RoborockInvalidCredentials( f"Invalid credentials ({home_id_response.get('msg')}) - check your login and try again." ) raise RoborockException(f"{home_id_response.get('msg')} - response code: {home_id_response.get('code')}") return home_id_response["data"]["rrHomeId"] async def get_home_data(self, user_data: UserData) -> HomeData: try: self._home_data_limiter.try_acquire("home_data") except BucketFullException as ex: _LOGGER.info(ex.meta_info) raise RoborockRateLimit("Reached maximum requests for home data. Please try again later.") from ex rriot = user_data.rriot if rriot is None: raise RoborockException("rriot is none") home_id = await self._get_home_id(user_data) if rriot.r.a is None: raise RoborockException("Missing field 'a' in rriot reference") home_request = PreparedRequest( rriot.r.a, self.session, { "Authorization": self._get_hawk_authentication(rriot, f"/user/homes/{str(home_id)}"), }, ) home_response = await home_request.request("get", "/user/homes/" + str(home_id)) if not home_response.get("success"): raise RoborockException(home_response) home_data = home_response.get("result") if isinstance(home_data, dict): return HomeData.from_dict(home_data) else: raise RoborockException("home_response result was an unexpected type") async def get_home_data_v2(self, user_data: UserData) -> HomeData: """This is the same as get_home_data, but uses a different endpoint and includes non-robotic vacuums.""" try: self._home_data_limiter.try_acquire("home_data") except BucketFullException as ex: _LOGGER.info(ex.meta_info) raise RoborockRateLimit("Reached maximum requests for home data. Please try again later.") from ex rriot = user_data.rriot if rriot is None: raise RoborockException("rriot is none") home_id = await self._get_home_id(user_data) if rriot.r.a is None: raise RoborockException("Missing field 'a' in rriot reference") home_request = PreparedRequest( rriot.r.a, self.session, { "Authorization": self._get_hawk_authentication(rriot, "/v2/user/homes/" + str(home_id)), }, ) home_response = await home_request.request("get", "/v2/user/homes/" + str(home_id)) if not home_response.get("success"): raise RoborockException(home_response) home_data = home_response.get("result") if isinstance(home_data, dict): return HomeData.from_dict(home_data) else: raise RoborockException("home_response result was an unexpected type") async def get_home_data_v3(self, user_data: UserData) -> HomeData: """This is the same as get_home_data, but uses a different endpoint and includes non-robotic vacuums.""" try: self._home_data_limiter.try_acquire("home_data") except BucketFullException as ex: _LOGGER.info(ex.meta_info) raise RoborockRateLimit("Reached maximum requests for home data. Please try again later.") from ex rriot = user_data.rriot home_id = await self._get_home_id(user_data) if rriot.r.a is None: raise RoborockException("Missing field 'a' in rriot reference") home_request = PreparedRequest( rriot.r.a, self.session, { "Authorization": self._get_hawk_authentication(rriot, "/v3/user/homes/" + str(home_id)), }, ) home_response = await home_request.request("get", "/v3/user/homes/" + str(home_id)) if not home_response.get("success"): raise RoborockException(home_response) home_data = home_response.get("result") if isinstance(home_data, dict): return HomeData.from_dict(home_data) raise RoborockException(f"home_response result was an unexpected type: {home_data}") async def get_rooms(self, user_data: UserData, home_id: int | None = None) -> list[HomeDataRoom]: rriot = user_data.rriot if rriot is None: raise RoborockException("rriot is none") if home_id is None: home_id = await self._get_home_id(user_data) if rriot.r.a is None: raise RoborockException("Missing field 'a' in rriot reference") room_request = PreparedRequest( rriot.r.a, self.session, { "Authorization": self._get_hawk_authentication(rriot, "/v2/user/homes/" + str(home_id)), }, ) room_response = await room_request.request("get", f"/user/homes/{str(home_id)}/rooms" + str(home_id)) if not room_response.get("success"): raise RoborockException(room_response) rooms = room_response.get("result") if isinstance(rooms, list): output_list = [] for room in rooms: output_list.append(HomeDataRoom.from_dict(room)) return output_list else: raise RoborockException("home_response result was an unexpected type") async def get_scenes(self, user_data: UserData, device_id: str) -> list[HomeDataScene]: rriot = user_data.rriot if rriot is None: raise RoborockException("rriot is none") if rriot.r.a is None: raise RoborockException("Missing field 'a' in rriot reference") scenes_request = PreparedRequest( rriot.r.a, self.session, { "Authorization": self._get_hawk_authentication(rriot, f"/user/scene/device/{str(device_id)}"), }, ) scenes_response = await scenes_request.request("get", f"/user/scene/device/{str(device_id)}") if not scenes_response.get("success"): raise RoborockException(scenes_response) scenes = scenes_response.get("result") if isinstance(scenes, list): return [HomeDataScene.from_dict(scene) for scene in scenes] else: raise RoborockException("scene_response result was an unexpected type") async def execute_scene(self, user_data: UserData, scene_id: int) -> None: rriot = user_data.rriot if rriot is None: raise RoborockException("rriot is none") if rriot.r.a is None: raise RoborockException("Missing field 'a' in rriot reference") execute_scene_request = PreparedRequest( rriot.r.a, self.session, { "Authorization": self._get_hawk_authentication(rriot, f"/user/scene/{str(scene_id)}/execute"), }, ) execute_scene_response = await execute_scene_request.request("POST", f"/user/scene/{str(scene_id)}/execute") if not execute_scene_response.get("success"): raise RoborockException(execute_scene_response) async def get_products(self, user_data: UserData) -> ProductResponse: """Gets all products and their schemas, good for determining status codes and model numbers.""" base_url = await self._get_base_url() header_clientid = self._get_header_client_id() product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) product_response = await product_request.request( "get", "/api/v4/product", headers={"Authorization": user_data.token}, ) if product_response is None: raise RoborockException("home_id_response is None") if product_response.get("code") != 200: raise RoborockException(f"{product_response.get('msg')} - response code: {product_response.get('code')}") result = product_response.get("data") if isinstance(result, dict): return ProductResponse.from_dict(result) raise RoborockException("product result was an unexpected type") async def download_code(self, user_data: UserData, product_id: int): base_url = await self._get_base_url() header_clientid = self._get_header_client_id() product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) request = {"apilevel": 99999, "productids": [product_id], "type": 2} response = await product_request.request( "post", "/api/v1/appplugin", json=request, headers={"Authorization": user_data.token, "Content-Type": "application/json"}, ) return response["data"][0]["url"] async def download_category_code(self, user_data: UserData): base_url = await self._get_base_url() header_clientid = self._get_header_client_id() product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) response = await product_request.request( "get", "api/v1/plugins?apiLevel=99999&type=2", headers={ "Authorization": user_data.token, }, ) return {r["category"]: r["url"] for r in response["data"]["categoryPluginList"]} class PreparedRequest: def __init__( self, base_url: str, session: aiohttp.ClientSession | None = None, base_headers: dict | None = None ) -> None: self.base_url = base_url self.base_headers = base_headers or {} self.session = session async def request(self, method: str, url: str, params=None, data=None, headers=None, json=None) -> dict: _url = "/".join(s.strip("/") for s in [self.base_url, url]) _headers = {**self.base_headers, **(headers or {})} close_session = self.session is None session = self.session if self.session is not None else aiohttp.ClientSession() try: async with session.request(method, _url, params=params, data=data, headers=_headers, json=json) as resp: return await resp.json() except ContentTypeError as err: """If we get an error, lets log everything for debugging.""" try: resp_json = await resp.json(content_type=None) _LOGGER.info("Resp: %s", resp_json) except ContentTypeError as err_2: _LOGGER.info(err_2) resp_raw = await resp.read() _LOGGER.info("Resp raw: %s", resp_raw) # Still raise the err so that it's clear it failed. raise err finally: if close_session: await session.close() python-roborock-2.19.0/tests/000077500000000000000000000000001501065527500161125ustar00rootroot00000000000000python-roborock-2.19.0/tests/__init__.py000066400000000000000000000000001501065527500202110ustar00rootroot00000000000000python-roborock-2.19.0/tests/conftest.py000066400000000000000000000310221501065527500203070ustar00rootroot00000000000000import asyncio import io import logging import re from asyncio import Protocol from collections.abc import AsyncGenerator, Callable, Generator from queue import Queue from typing import Any from unittest.mock import Mock, patch import pytest from aioresponses import aioresponses from roborock import HomeData, UserData from roborock.containers import DeviceData from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1 from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 from tests.mock_data import HOME_DATA_RAW, HOME_DATA_SCENES_RAW, TEST_LOCAL_API_HOST, USER_DATA _LOGGER = logging.getLogger(__name__) # Used by fixtures to handle incoming requests and prepare responses RequestHandler = Callable[[bytes], bytes | None] QUEUE_TIMEOUT = 10 class FakeSocketHandler: """Fake socket used by the test to simulate a connection to the broker. The socket handler is used to intercept the socket send and recv calls and populate the response buffer with data to be sent back to the client. The handle request callback handles the incoming requests and prepares the responses. """ def __init__(self, handle_request: RequestHandler, response_queue: Queue[bytes]) -> None: self.response_buf = io.BytesIO() self.handle_request = handle_request self.response_queue = response_queue def pending(self) -> int: """Return the number of bytes in the response buffer.""" return len(self.response_buf.getvalue()) def handle_socket_recv(self, read_size: int) -> bytes: """Intercept a client recv() and populate the buffer.""" if self.pending() == 0: raise BlockingIOError("No response queued") self.response_buf.seek(0) data = self.response_buf.read(read_size) _LOGGER.debug("Response: 0x%s", data.hex()) # Consume the rest of the data in the buffer remaining_data = self.response_buf.read() self.response_buf = io.BytesIO(remaining_data) return data def handle_socket_send(self, client_request: bytes) -> int: """Receive an incoming request from the client.""" _LOGGER.debug("Request: 0x%s", client_request.hex()) if (response := self.handle_request(client_request)) is not None: # Enqueue a response to be sent back to the client in the buffer. # The buffer will be emptied when the client calls recv() on the socket _LOGGER.debug("Queued: 0x%s", response.hex()) self.response_buf.write(response) return len(client_request) def push_response(self) -> None: """Push a response to the client.""" if not self.response_queue.empty(): response = self.response_queue.get() # Enqueue a response to be sent back to the client in the buffer. # The buffer will be emptied when the client calls recv() on the socket _LOGGER.debug("Queued: 0x%s", response.hex()) self.response_buf.write(response) @pytest.fixture(name="received_requests") def received_requests_fixture() -> Queue[bytes]: """Fixture that provides access to the received requests.""" return Queue() @pytest.fixture(name="response_queue") def response_queue_fixture() -> Generator[Queue[bytes], None, None]: """Fixture that provides access to the received requests.""" response_queue: Queue[bytes] = Queue() yield response_queue assert response_queue.empty(), "Not all fake responses were consumed" @pytest.fixture(name="request_handler") def request_handler_fixture(received_requests: Queue[bytes], response_queue: Queue[bytes]) -> RequestHandler: """Fixture records incoming requests and replies with responses from the queue.""" def handle_request(client_request: bytes) -> bytes | None: """Handle an incoming request from the client.""" received_requests.put(client_request) # Insert a prepared response into the response buffer if not response_queue.empty(): return response_queue.get() return None return handle_request @pytest.fixture(name="fake_socket_handler") def fake_socket_handler_fixture(request_handler: RequestHandler, response_queue: Queue[bytes]) -> FakeSocketHandler: """Fixture that creates a fake MQTT broker.""" return FakeSocketHandler(request_handler, response_queue) @pytest.fixture(name="mock_sock") def mock_sock_fixture(fake_socket_handler: FakeSocketHandler) -> Mock: """Fixture that creates a mock socket connection and wires it to the handler.""" mock_sock = Mock() mock_sock.recv = fake_socket_handler.handle_socket_recv mock_sock.send = fake_socket_handler.handle_socket_send mock_sock.pending = fake_socket_handler.pending return mock_sock @pytest.fixture(name="mock_create_connection") def create_connection_fixture(mock_sock: Mock) -> Generator[None, None, None]: """Fixture that overrides the MQTT socket creation to wire it up to the mock socket.""" with patch("paho.mqtt.client.socket.create_connection", return_value=mock_sock): yield @pytest.fixture(name="mock_select") def select_fixture(mock_sock: Mock, fake_socket_handler: FakeSocketHandler) -> Generator[None, None, None]: """Fixture that overrides the MQTT client select calls to make select work on the mock socket. This patch select to activate our mock socket when ready with data. Internal mqtt sockets are always ready since they are used internally to wake the select loop. Ours is ready if there is data in the buffer. """ def is_ready(sock: Any) -> bool: return sock is not mock_sock or (fake_socket_handler.pending() > 0) def handle_select(rlist: list, wlist: list, *args: Any) -> list: return [list(filter(is_ready, rlist)), list(filter(is_ready, wlist))] with patch("paho.mqtt.client.select.select", side_effect=handle_select): yield @pytest.fixture(name="mqtt_client") async def mqtt_client(mock_create_connection: None, mock_select: None) -> AsyncGenerator[RoborockMqttClientV1, None]: user_data = UserData.from_dict(USER_DATA) home_data = HomeData.from_dict(HOME_DATA_RAW) device_info = DeviceData( device=home_data.devices[0], model=home_data.products[0].model, ) client = RoborockMqttClientV1(user_data, device_info, queue_timeout=QUEUE_TIMEOUT) try: yield client finally: if not client.is_connected(): try: await client.async_release() except Exception: pass @pytest.fixture(name="mock_rest", autouse=True) def mock_rest() -> aioresponses: """Mock all rest endpoints so they won't hit real endpoints""" with aioresponses() as mocked: # Match the base URL and allow any query params mocked.post( re.compile(r"https://euiot\.roborock\.com/api/v1/getUrlByEmail.*"), status=200, payload={ "code": 200, "data": {"country": "US", "countrycode": "1", "url": "https://usiot.roborock.com"}, "msg": "success", }, ) mocked.post( re.compile(r"https://.*iot\.roborock\.com/api/v1/login.*"), status=200, payload={"code": 200, "data": USER_DATA, "msg": "success"}, ) mocked.post( re.compile(r"https://.*iot\.roborock\.com/api/v1/loginWithCode.*"), status=200, payload={"code": 200, "data": USER_DATA, "msg": "success"}, ) mocked.post( re.compile(r"https://.*iot\.roborock\.com/api/v1/sendEmailCode.*"), status=200, payload={"code": 200, "data": None, "msg": "success"}, ) mocked.get( re.compile(r"https://.*iot\.roborock\.com/api/v1/getHomeDetail.*"), status=200, payload={ "code": 200, "data": {"deviceListOrder": None, "id": 123456, "name": "My Home", "rrHomeId": 123456, "tuyaHomeId": 0}, "msg": "success", }, ) mocked.get( re.compile(r"https://api-.*\.roborock\.com/v2/user/homes*"), status=200, payload={"api": None, "code": 200, "result": HOME_DATA_RAW, "status": "ok", "success": True}, ) mocked.post( re.compile(r"https://api-.*\.roborock\.com/nc/prepare"), status=200, payload={ "api": None, "result": {"r": "US", "s": "ffffff", "t": "eOf6d2BBBB"}, "status": "ok", "success": True, }, ) mocked.get( re.compile(r"https://api-.*\.roborock\.com/user/devices/newadd/*"), status=200, payload={ "api": "获取新增设备信息", "result": { "activeTime": 1737724598, "attribute": None, "cid": None, "createTime": 0, "deviceStatus": None, "duid": "rand_duid", "extra": "{}", "f": False, "featureSet": "0", "fv": "02.16.12", "iconUrl": "", "lat": None, "localKey": "random_lk", "lon": None, "name": "S7", "newFeatureSet": "0000000000002000", "online": True, "productId": "rand_prod_id", "pv": "1.0", "roomId": None, "runtimeEnv": None, "setting": None, "share": False, "shareTime": None, "silentOtaSwitch": False, "sn": "Rand_sn", "timeZoneId": "America/New_York", "tuyaMigrated": False, "tuyaUuid": None, }, "status": "ok", "success": True, }, ) mocked.get( re.compile(r"https://api-.*\.roborock\.com/user/scene/device/.*"), status=200, payload={"api": None, "code": 200, "result": HOME_DATA_SCENES_RAW, "status": "ok", "success": True}, ) mocked.post( re.compile(r"https://api-.*\.roborock\.com/user/scene/.*/execute"), status=200, payload={"api": None, "code": 200, "result": None, "status": "ok", "success": True}, ) yield mocked @pytest.fixture(autouse=True) def skip_rate_limit(): """Don't rate limit tests as they aren't actually hitting the api.""" with ( patch("roborock.web_api.RoborockApiClient._login_limiter.try_acquire"), patch("roborock.web_api.RoborockApiClient._home_data_limiter.try_acquire"), ): yield @pytest.fixture(name="mock_create_local_connection") def create_local_connection_fixture(request_handler: RequestHandler) -> Generator[None, None, None]: """Fixture that overrides the transport creation to wire it up to the mock socket.""" async def create_connection(protocol_factory: Callable[[], Protocol], *args) -> tuple[Any, Any]: protocol = protocol_factory() def handle_write(data: bytes) -> None: _LOGGER.debug("Received: %s", data) response = request_handler(data) if response is not None: _LOGGER.debug("Replying with %s", response) loop = asyncio.get_running_loop() loop.call_soon(protocol.data_received, response) closed = asyncio.Event() mock_transport = Mock() mock_transport.write = handle_write mock_transport.close = closed.set mock_transport.is_reading = lambda: not closed.is_set() return (mock_transport, "proto") with patch("roborock.local_api.get_running_loop") as mock_loop: mock_loop.return_value.create_connection.side_effect = create_connection yield @pytest.fixture(name="local_client") async def local_client_fixture(mock_create_local_connection: None) -> AsyncGenerator[RoborockLocalClientV1, None]: home_data = HomeData.from_dict(HOME_DATA_RAW) device_info = DeviceData( device=home_data.devices[0], model=home_data.products[0].model, host=TEST_LOCAL_API_HOST, ) client = RoborockLocalClientV1(device_info, queue_timeout=QUEUE_TIMEOUT) try: yield client finally: if not client.is_connected(): try: await client.async_release() except Exception: pass python-roborock-2.19.0/tests/mock_data.py000066400000000000000000000537761501065527500204300ustar00rootroot00000000000000"""Mock data for Roborock tests.""" import hashlib import json # All data is based on a U.S. customer with a Roborock S7 MaxV Ultra USER_EMAIL = "user@domain.com" BASE_URL = "https://usiot.roborock.com" USER_ID = "user123" K_VALUE = "domain123" USER_DATA = { "uid": 123456, "tokentype": "token_type", "token": "abc123", "rruid": "abc123", "region": "us", "countrycode": "1", "country": "US", "nickname": "user_nickname", "rriot": { "u": USER_ID, "s": "pass123", "h": "unknown123", "k": K_VALUE, "r": { "r": "US", "a": "https://api-us.roborock.com", "m": "tcp://mqtt-us.roborock.com:8883", # Skip SSL code in MQTT client library "l": "https://wood-us.roborock.com", }, }, "tuyaDeviceState": 2, "avatarurl": "https://files.roborock.com/iottest/default_avatar.png", } LOCAL_KEY = "key123key123key1" # 16 bytes / 128 bits PRODUCT_ID = "product-id-123" HOME_DATA_SCENES_RAW = [ { "id": 1234567, "name": "My plan", "param": json.dumps( { "triggers": [], "action": { "type": "S", "items": [ { "id": 5, "type": "CMD", "name": "", "entityId": "EEEEEEEEEEEEEE", "param": json.dumps( { "id": 5, "method": "do_scenes_app_start", "params": [ { "fan_power": 104, "water_box_mode": 200, "mop_mode": 300, "mop_template_id": 300, "repeat": 1, "auto_dustCollection": 1, "source": 101, } ], } ), "finishDpIds": [130], }, { "id": 4, "type": "CMD", "name": "", "entityId": "EEEEEEEEEEEEEE", "param": json.dumps( { "id": 4, "method": "do_scenes_segments", "params": { "data": [ { "tid": "111111111111111111", "segs": [ {"sid": 19}, {"sid": 18}, {"sid": 22}, {"sid": 21}, {"sid": 16}, ], "map_flag": 0, "fan_power": 105, "water_box_mode": 201, "mop_mode": 300, "mop_template_id": 300, "repeat": 1, "clean_order_mode": 1, "auto_dry": 1, "auto_dustCollection": 1, "region_num": 0, } ], "source": 101, }, } ), "finishDpIds": [130], }, ], }, "matchType": "NONE", "tagId": "4444", } ), "enabled": True, "extra": None, "type": "WORKFLOW", } ] HOME_DATA_RAW = { "id": 123456, "name": "My Home", "lon": None, "lat": None, "geoName": None, "products": [ { "id": PRODUCT_ID, "name": "Roborock S7 MaxV", "code": "a27", "model": "roborock.vacuum.a27", "iconUrl": None, "attribute": None, "capability": 0, "category": "robot.vacuum.cleaner", "schema": [ { "id": "101", "name": "rpc_request", "code": "rpc_request_code", "mode": "rw", "type": "RAW", "property": None, "desc": None, }, { "id": "102", "name": "rpc_response", "code": "rpc_response", "mode": "rw", "type": "RAW", "property": None, "desc": None, }, { "id": "120", "name": "错误代码", "code": "error_code", "mode": "ro", "type": "ENUM", "property": '{"range": []}', "desc": None, }, { "id": "121", "name": "设备状态", "code": "state", "mode": "ro", "type": "ENUM", "property": '{"range": []}', "desc": None, }, { "id": "122", "name": "设备电量", "code": "battery", "mode": "ro", "type": "ENUM", "property": '{"range": []}', "desc": None, }, { "id": "123", "name": "清扫模式", "code": "fan_power", "mode": "rw", "type": "ENUM", "property": '{"range": []}', "desc": None, }, { "id": "124", "name": "拖地模式", "code": "water_box_mode", "mode": "rw", "type": "ENUM", "property": '{"range": []}', "desc": None, }, { "id": "125", "name": "主刷寿命", "code": "main_brush_life", "mode": "rw", "type": "VALUE", "property": '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', "desc": None, }, { "id": "126", "name": "边刷寿命", "code": "side_brush_life", "mode": "rw", "type": "VALUE", "property": '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', "desc": None, }, { "id": "127", "name": "滤网寿命", "code": "filter_life", "mode": "rw", "type": "VALUE", "property": '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', "desc": None, }, { "id": "128", "name": "额外状态", "code": "additional_props", "mode": "ro", "type": "RAW", "property": None, "desc": None, }, { "id": "130", "name": "完成事件", "code": "task_complete", "mode": "ro", "type": "RAW", "property": None, "desc": None, }, { "id": "131", "name": "电量不足任务取消", "code": "task_cancel_low_power", "mode": "ro", "type": "RAW", "property": None, "desc": None, }, { "id": "132", "name": "运动中任务取消", "code": "task_cancel_in_motion", "mode": "ro", "type": "RAW", "property": None, "desc": None, }, { "id": "133", "name": "充电状态", "code": "charge_status", "mode": "ro", "type": "RAW", "property": None, "desc": None, }, { "id": "134", "name": "烘干状态", "code": "drying_status", "mode": "ro", "type": "RAW", "property": None, "desc": None, }, ], } ], "devices": [ { "duid": "abc123", "name": "Roborock S7 MaxV", "attribute": None, "activeTime": 1672364449, "localKey": LOCAL_KEY, "runtimeEnv": None, "timeZoneId": "America/Los_Angeles", "iconUrl": "no_url", "productId": "product123", "lon": None, "lat": None, "share": False, "shareTime": None, "online": True, "fv": "02.56.02", "pv": "1.0", "roomId": 2362003, "tuyaUuid": None, "tuyaMigrated": False, "extra": '{"RRPhotoPrivacyVersion": "1"}', "sn": "abc123", "featureSet": "2234201184108543", "newFeatureSet": "0000000000002041", "deviceStatus": { "121": 8, "122": 100, "123": 102, "124": 203, "125": 94, "126": 90, "127": 87, "128": 0, "133": 1, "120": 0, }, "silentOtaSwitch": True, } ], "receivedDevices": [], "rooms": [ {"id": 2362048, "name": "Example room 1"}, {"id": 2362044, "name": "Example room 2"}, {"id": 2362041, "name": "Example room 3"}, ], } WASHER_PRODUCT = { "id": PRODUCT_ID, "name": "Zeo One", "model": "roborock.wm.a102", "category": "roborock.wm", "capability": 2, "schema": [ { "id": "134", "name": "烘干状态", "code": "drying_status", "mode": "ro", "type": "RAW", }, { "id": "200", "name": "启动", "code": "start", "mode": "rw", "type": "BOOL", }, { "id": "201", "name": "暂停", "code": "pause", "mode": "rw", "type": "BOOL", }, { "id": "202", "name": "关机", "code": "shutdown", "mode": "rw", "type": "BOOL", }, { "id": "203", "name": "状态", "code": "status", "mode": "ro", "type": "VALUE", }, { "id": "204", "name": "模式", "code": "mode", "mode": "rw", "type": "VALUE", }, { "id": "205", "name": "程序", "code": "program", "mode": "rw", "type": "VALUE", }, { "id": "206", "name": "童锁", "code": "child_lock", "mode": "rw", "type": "BOOL", }, { "id": "207", "name": "洗涤温度", "code": "temp", "mode": "rw", "type": "VALUE", }, { "id": "208", "name": "漂洗次数", "code": "rinse_times", "mode": "rw", "type": "VALUE", }, { "id": "209", "name": "滚筒转速", "code": "spin_level", "mode": "rw", "type": "VALUE", }, { "id": "210", "name": "干燥度", "code": "drying_mode", "mode": "rw", "type": "VALUE", }, { "id": "211", "name": "自动投放-洗衣液", "code": "detergent_set", "mode": "rw", "type": "BOOL", }, { "id": "212", "name": "自动投放-柔顺剂", "code": "softener_set", "mode": "rw", "type": "BOOL", }, { "id": "213", "name": "洗衣液投放量", "code": "detergent_type", "mode": "rw", "type": "VALUE", }, { "id": "214", "name": "柔顺剂投放量", "code": "softener_type", "mode": "rw", "type": "VALUE", }, { "id": "217", "name": "预约时间", "code": "countdown", "mode": "rw", "type": "VALUE", }, { "id": "218", "name": "洗衣剩余时间", "code": "washing_left", "mode": "ro", "type": "VALUE", }, { "id": "219", "name": "门锁状态", "code": "doorlock_state", "mode": "ro", "type": "BOOL", }, { "id": "220", "name": "故障", "code": "error", "mode": "ro", "type": "VALUE", }, { "id": "221", "name": "云程序设置", "code": "custom_param_save", "mode": "rw", "type": "VALUE", }, { "id": "222", "name": "云程序读取", "code": "custom_param_get", "mode": "ro", "type": "VALUE", }, { "id": "223", "name": "提示音", "code": "sound_set", "mode": "rw", "type": "BOOL", }, { "id": "224", "name": "距离上次筒自洁次数", "code": "times_after_clean", "mode": "ro", "type": "VALUE", }, { "id": "225", "name": "记忆洗衣偏好开关", "code": "default_setting", "mode": "rw", "type": "BOOL", }, { "id": "226", "name": "洗衣液用尽", "code": "detergent_empty", "mode": "ro", "type": "BOOL", }, { "id": "227", "name": "柔顺剂用尽", "code": "softener_empty", "mode": "ro", "type": "BOOL", }, { "id": "229", "name": "筒灯设定", "code": "light_setting", "mode": "rw", "type": "BOOL", }, { "id": "230", "name": "洗衣液投放量(单次)", "code": "detergent_volume", "mode": "rw", "type": "VALUE", }, { "id": "231", "name": "柔顺剂投放量(单次)", "code": "softener_volume", "mode": "rw", "type": "VALUE", }, { "id": "232", "name": "远程控制授权", "code": "app_authorization", "mode": "rw", "type": "VALUE", }, { "id": "10000", "name": "ID点查询", "code": "id_query", "mode": "rw", "type": "STRING", }, { "id": "10001", "name": "防串货", "code": "f_c", "mode": "ro", "type": "STRING", }, { "id": "10004", "name": "语音包/OBA信息", "code": "snd_state", "mode": "rw", "type": "STRING", }, { "id": "10005", "name": "产品信息", "code": "product_info", "mode": "ro", "type": "STRING", }, { "id": "10006", "name": "隐私协议", "code": "privacy_info", "mode": "rw", "type": "STRING", }, { "id": "10007", "name": "OTA info", "code": "ota_nfo", "mode": "rw", "type": "STRING", }, { "id": "10008", "name": "洗衣记录", "code": "washing_log", "mode": "ro", "type": "BOOL", }, { "id": "10101", "name": "rpc req", "code": "rpc_req", "mode": "wo", "type": "STRING", }, { "id": "10102", "name": "rpc resp", "code": "rpc_resp", "mode": "ro", "type": "STRING", }, ], } ZEO_ONE_DEVICE = { "duid": "zeo_duid", "name": "Zeo One", "localKey": LOCAL_KEY, "fv": "01.00.94", "productId": PRODUCT_ID, "activeTime": 1699964128, "timeZoneId": "Europe/Berlin", "iconUrl": "", "share": True, "shareTime": 1712763572, "online": True, "pv": "A01", "tuyaMigrated": False, "sn": "zeo_sn", "featureSet": "0", "newFeatureSet": "40", "deviceStatus": { "208": 2, "205": 33, "221": 0, "226": 0, "10001": '{"f":"t"}', "214": 2, "225": 0, "232": 0, "222": 347414, "206": 0, "200": 1, "219": 0, "223": 0, "220": 0, "201": 0, "202": 1, "10005": '{"sn":"zeo_sn","ssid":"internet","timezone":"Europe/Berlin","posix_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ip":"192.111.11.11","mac":"b0:4a:00:00:00:00","rssi":-57,"oba":{"language":"en","name":"A.03.0403_CE","bom":"A.03.0403","location":"de","wifiplan":"EU","timezone":"CET-1CEST,M3.5.0,M10.5.0/3;Europe/Berlin","logserver":"awsde0","loglevel":"4","featureset":"0"}}', # noqa: E501 "211": 1, "210": 1, "217": 0, "203": 7, "213": 2, "209": 7, "224": 21, "218": 227, "212": 1, "207": 4, "204": 1, "10007": '{"mqttOtaData":{"mqttOtaStatus":{"status":"IDLE"}}}', "227": 1, }, "silentOtaSwitch": False, "f": False, } CLEAN_RECORD = { "begin": 1672543330, "end": 1672544638, "duration": 1176, "area": 20965000, "error": 0, "complete": 1, "start_type": 2, "clean_type": 3, "finish_reason": 56, "dust_collection_status": 1, "avoid_count": 19, "wash_count": 2, "map_flag": 0, } CLEAN_SUMMARY = { "clean_time": 74382, "clean_area": 1159182500, "clean_count": 31, "dust_collection_count": 25, "records": [ 1672543330, 1672458041, ], } CONSUMABLE = { "main_brush_work_time": 74382, "side_brush_work_time": 74383, "filter_work_time": 74384, "filter_element_work_time": 0, "sensor_dirty_time": 74385, "strainer_work_times": 65, "dust_collection_work_times": 25, "cleaning_brush_work_times": 66, } DND_TIMER = { "start_hour": 22, "start_minute": 0, "end_hour": 7, "end_minute": 0, "enabled": 1, } STATUS = { "msg_ver": 2, "msg_seq": 458, "state": 8, "battery": 100, "clean_time": 1176, "clean_area": 20965000, "error_code": 0, "map_present": 1, "in_cleaning": 0, "in_returning": 0, "in_fresh_state": 1, "lab_status": 1, "water_box_status": 1, "back_type": -1, "wash_phase": 0, "wash_ready": 0, "fan_power": 102, "dnd_enabled": 0, "map_status": 3, "is_locating": 0, "lock_status": 0, "water_box_mode": 203, "water_box_carriage_status": 1, "mop_forbidden_enable": 1, "camera_status": 3457, "is_exploring": 0, "home_sec_status": 0, "home_sec_enable_password": 0, "adbumper_status": [0, 0, 0], "water_shortage_status": 0, "dock_type": 3, "dust_collection_status": 0, "auto_dust_collection": 1, "avoid_count": 19, "mop_mode": 300, "debug_mode": 0, "collision_avoid_status": 1, "switch_map_mode": 0, "dock_error_status": 0, "charge_status": 1, "unsave_map_reason": 0, "unsave_map_flag": 0, } BASE_URL_REQUEST = { "code": 200, "msg": "success", "data": {"url": "https://sample.com"}, } GET_CODE_RESPONSE = {"code": 200, "msg": "success", "data": None} HASHED_USER = hashlib.md5((USER_ID + ":" + K_VALUE).encode()).hexdigest()[2:10] MQTT_PUBLISH_TOPIC = f"rr/m/o/{USER_ID}/{HASHED_USER}/{PRODUCT_ID}" TEST_LOCAL_API_HOST = "1.1.1.1" python-roborock-2.19.0/tests/mqtt/000077500000000000000000000000001501065527500170775ustar00rootroot00000000000000python-roborock-2.19.0/tests/mqtt/test_roborock_session.py000066400000000000000000000163511501065527500241010ustar00rootroot00000000000000"""Tests for the MQTT session module.""" import asyncio from collections.abc import Callable, Generator from queue import Queue from typing import Any from unittest.mock import AsyncMock, Mock, patch import aiomqtt import paho.mqtt.client as mqtt import pytest from roborock.mqtt.roborock_session import create_mqtt_session from roborock.mqtt.session import MqttParams, MqttSessionException from tests import mqtt_packet from tests.conftest import FakeSocketHandler # We mock out the connection so these params are not used/verified FAKE_PARAMS = MqttParams( host="localhost", port=1883, tls=False, username="username", password="password", timeout=10.0, ) @pytest.fixture(autouse=True) def mqtt_server_fixture(mock_create_connection: None, mock_select: None) -> None: """Fixture to prepare a fake MQTT server.""" @pytest.fixture(autouse=True) def mock_client_fixture(event_loop: asyncio.AbstractEventLoop) -> Generator[None, None, None]: """Fixture to patch the MQTT underlying sync client. The tests use fake sockets, so this ensures that the async mqtt client does not attempt to listen on them directly. We instead just poll the socket for data ourselves. """ orig_class = mqtt.Client async def poll_sockets(client: mqtt.Client) -> None: """Poll the mqtt client sockets in a loop to pick up new data.""" while True: event_loop.call_soon_threadsafe(client.loop_read) event_loop.call_soon_threadsafe(client.loop_write) await asyncio.sleep(0.1) task: asyncio.Task[None] | None = None def new_client(*args: Any, **kwargs: Any) -> mqtt.Client: """Create a new mqtt client and start the socket polling task.""" nonlocal task client = orig_class(*args, **kwargs) task = event_loop.create_task(poll_sockets(client)) return client with patch("aiomqtt.client.Client._on_socket_open"), patch("aiomqtt.client.Client._on_socket_close"), patch( "aiomqtt.client.Client._on_socket_register_write" ), patch("aiomqtt.client.Client._on_socket_unregister_write"), patch( "aiomqtt.client.mqtt.Client", side_effect=new_client ): yield if task: task.cancel() @pytest.fixture def push_response(response_queue: Queue, fake_socket_handler: FakeSocketHandler) -> Callable[[bytes], None]: """Fixtures to push messages.""" def push(message: bytes) -> None: response_queue.put(message) fake_socket_handler.push_response() return push class Subscriber: """Mock subscriber class. This will capture messages published on the session so the tests can verify they were received. """ def __init__(self) -> None: """Initialize the subscriber.""" self.messages: list[bytes] = [] self.event: asyncio.Event = asyncio.Event() def append(self, message: bytes) -> None: """Append a message to the subscriber.""" self.messages.append(message) self.event.set() async def wait(self) -> None: """Wait for a message to be received.""" await self.event.wait() self.event.clear() async def test_session(push_response: Callable[[bytes], None]) -> None: """Test the MQTT session.""" push_response(mqtt_packet.gen_connack(rc=0, flags=2)) session = await create_mqtt_session(FAKE_PARAMS) assert session.connected push_response(mqtt_packet.gen_suback(mid=1)) subscriber1 = Subscriber() unsub1 = await session.subscribe("topic-1", subscriber1.append) push_response(mqtt_packet.gen_suback(mid=2)) subscriber2 = Subscriber() await session.subscribe("topic-2", subscriber2.append) push_response(mqtt_packet.gen_publish("topic-1", mid=3, payload=b"12345")) await subscriber1.wait() assert subscriber1.messages == [b"12345"] assert not subscriber2.messages push_response(mqtt_packet.gen_publish("topic-2", mid=4, payload=b"67890")) await subscriber2.wait() assert subscriber2.messages == [b"67890"] push_response(mqtt_packet.gen_publish("topic-1", mid=5, payload=b"ABC")) await subscriber1.wait() assert subscriber1.messages == [b"12345", b"ABC"] assert subscriber2.messages == [b"67890"] # Messages are no longer received after unsubscribing unsub1() push_response(mqtt_packet.gen_publish("topic-1", payload=b"ignored")) assert subscriber1.messages == [b"12345", b"ABC"] assert session.connected await session.close() assert not session.connected async def test_session_no_subscribers(push_response: Callable[[bytes], None]) -> None: """Test the MQTT session.""" push_response(mqtt_packet.gen_connack(rc=0, flags=2)) push_response(mqtt_packet.gen_publish("topic-1", mid=3, payload=b"12345")) push_response(mqtt_packet.gen_publish("topic-2", mid=4, payload=b"67890")) session = await create_mqtt_session(FAKE_PARAMS) assert session.connected await session.close() assert not session.connected async def test_publish_command(push_response: Callable[[bytes], None]) -> None: """Test publishing during an MQTT session.""" push_response(mqtt_packet.gen_connack(rc=0, flags=2)) session = await create_mqtt_session(FAKE_PARAMS) push_response(mqtt_packet.gen_publish("topic-1", mid=3, payload=b"12345")) await session.publish("topic-1", message=b"payload") assert session.connected await session.close() assert not session.connected class FakeAsyncIterator: """Fake async iterator that waits for messages to arrive, but they never do. This is used for testing exceptions in other client functions. """ def __aiter__(self): return self async def __anext__(self) -> None: """Iterator that does not generate any messages.""" while True: await asyncio.sleep(1) async def test_publish_failure() -> None: """Test an MQTT error is received when publishing a message.""" mock_client = AsyncMock() mock_client.messages = FakeAsyncIterator() mock_aenter = AsyncMock() mock_aenter.return_value = mock_client with patch("roborock.mqtt.roborock_session.aiomqtt.Client.__aenter__", mock_aenter): session = await create_mqtt_session(FAKE_PARAMS) assert session.connected mock_client.publish.side_effect = aiomqtt.MqttError with pytest.raises(MqttSessionException, match="Error publishing message"): await session.publish("topic-1", message=b"payload") async def test_subscribe_failure() -> None: """Test an MQTT error while subscribing.""" mock_client = AsyncMock() mock_client.messages = FakeAsyncIterator() mock_aenter = AsyncMock() mock_aenter.return_value = mock_client mock_shim = Mock() mock_shim.return_value.__aenter__ = mock_aenter mock_shim.return_value.__aexit__ = AsyncMock() with patch("roborock.mqtt.roborock_session.aiomqtt.Client", mock_shim): session = await create_mqtt_session(FAKE_PARAMS) assert session.connected mock_client.subscribe.side_effect = aiomqtt.MqttError subscriber1 = Subscriber() with pytest.raises(MqttSessionException, match="Error subscribing to topic"): await session.subscribe("topic-1", subscriber1.append) assert not subscriber1.messages python-roborock-2.19.0/tests/mqtt_packet.py000066400000000000000000000072161501065527500210060ustar00rootroot00000000000000"""Module for crafting MQTT packets. This library is copied from the paho mqtt client library tests, with just the parts needed for some roborock messages. This message format in this file is not specific to roborock. """ import struct PROP_RECEIVE_MAXIMUM = 33 PROP_TOPIC_ALIAS_MAXIMUM = 34 def gen_uint16_prop(identifier: int, word: int) -> bytes: """Generate a property with a uint16_t value.""" prop = struct.pack("!BH", identifier, word) return prop def pack_varint(varint: int) -> bytes: """Pack a variable integer.""" s = b"" while True: byte = varint % 128 varint = varint // 128 # If there are more digits to encode, set the top bit of this digit if varint > 0: byte = byte | 0x80 s = s + struct.pack("!B", byte) if varint == 0: return s def prop_finalise(props: bytes) -> bytes: """Finalise the properties.""" if props is None: return pack_varint(0) else: return pack_varint(len(props)) + props def gen_connack(flags=0, rc=0, properties=b"", property_helper=True): """Generate a CONNACK packet.""" if property_helper: if properties is not None: properties = ( gen_uint16_prop(PROP_TOPIC_ALIAS_MAXIMUM, 10) + properties + gen_uint16_prop(PROP_RECEIVE_MAXIMUM, 20) ) else: properties = b"" properties = prop_finalise(properties) packet = struct.pack("!BBBB", 32, 2 + len(properties), flags, rc) + properties return packet def gen_suback(mid: int, qos: int = 0) -> bytes: """Generate a SUBACK packet.""" return struct.pack("!BBHBB", 144, 2 + 1 + 1, mid, 0, qos) def _gen_short(cmd: int, reason_code: int) -> bytes: return struct.pack("!BBB", cmd, 1, reason_code) def gen_disconnect(reason_code: int = 0) -> bytes: """Generate a DISCONNECT packet.""" return _gen_short(0xE0, reason_code) def _gen_command_with_mid(cmd: int, mid: int, reason_code: int = 0) -> bytes: return struct.pack("!BBHB", cmd, 3, mid, reason_code) def gen_puback(mid: int, reason_code: int = 0) -> bytes: """Generate a PUBACK packet.""" return _gen_command_with_mid(64, mid, reason_code) def _pack_remaining_length(remaining_length: int) -> bytes: """Pack a remaining length.""" s = b"" while True: byte = remaining_length % 128 remaining_length = remaining_length // 128 # If there are more digits to encode, set the top bit of this digit if remaining_length > 0: byte = byte | 0x80 s = s + struct.pack("!B", byte) if remaining_length == 0: return s def gen_publish( topic: str, payload: bytes | None = None, retain: bool = False, dup: bool = False, mid: int = 0, properties: bytes = b"", ) -> bytes: """Generate a PUBLISH packet.""" if isinstance(topic, str): topic_b = topic.encode("utf-8") rl = 2 + len(topic_b) pack_format = "H" + str(len(topic_b)) + "s" properties = prop_finalise(properties) rl += len(properties) # This will break if len(properties) > 127 pack_format = pack_format + "%ds" % (len(properties)) if payload is not None: # payload = payload.encode("utf-8") rl = rl + len(payload) pack_format = pack_format + str(len(payload)) + "s" else: payload = b"" pack_format = pack_format + "0s" rlpacked = _pack_remaining_length(rl) cmd = 48 if retain: cmd = cmd + 1 if dup: cmd = cmd + 8 return struct.pack( "!B" + str(len(rlpacked)) + "s" + pack_format, cmd, rlpacked, len(topic_b), topic_b, properties, payload ) python-roborock-2.19.0/tests/test_a01_api.py000066400000000000000000000166621501065527500207500ustar00rootroot00000000000000import asyncio import json from collections.abc import AsyncGenerator from queue import Queue from typing import Any from unittest.mock import patch import paho.mqtt.client as mqtt import pytest from Crypto.Cipher import AES from Crypto.Util.Padding import pad from roborock import ( HomeData, UserData, ) from roborock.containers import DeviceData, RoborockCategory from roborock.exceptions import RoborockException from roborock.protocol import MessageParser from roborock.roborock_message import ( RoborockMessage, RoborockMessageProtocol, RoborockZeoProtocol, ) from roborock.version_a01_apis import RoborockMqttClientA01 from tests.mock_data import ( HOME_DATA_RAW, LOCAL_KEY, MQTT_PUBLISH_TOPIC, USER_DATA, WASHER_PRODUCT, ZEO_ONE_DEVICE, ) from . import mqtt_packet from .conftest import QUEUE_TIMEOUT @pytest.fixture(name="a01_mqtt_client") async def a01_mqtt_client_fixture( mock_create_connection: None, mock_select: None ) -> AsyncGenerator[RoborockMqttClientA01, None]: user_data = UserData.from_dict(USER_DATA) home_data = HomeData.from_dict( { **HOME_DATA_RAW, "devices": [ZEO_ONE_DEVICE], "products": [WASHER_PRODUCT], } ) device_info = DeviceData( device=home_data.devices[0], model=home_data.products[0].model, ) client = RoborockMqttClientA01( user_data, device_info, RoborockCategory.WASHING_MACHINE, queue_timeout=QUEUE_TIMEOUT ) try: yield client finally: if not client.is_connected(): try: await client.async_release() except Exception: pass @pytest.fixture(name="connected_a01_mqtt_client") async def connected_a01_mqtt_client_fixture( response_queue: Queue, a01_mqtt_client: RoborockMqttClientA01 ) -> AsyncGenerator[RoborockMqttClientA01, None]: response_queue.put(mqtt_packet.gen_connack(rc=0, flags=2)) response_queue.put(mqtt_packet.gen_suback(1, 0)) await a01_mqtt_client.async_connect() yield a01_mqtt_client async def test_async_connect(received_requests: Queue, connected_a01_mqtt_client: RoborockMqttClientA01) -> None: """Test connecting to the MQTT broker.""" assert connected_a01_mqtt_client.is_connected() # Connecting again is a no-op await connected_a01_mqtt_client.async_connect() assert connected_a01_mqtt_client.is_connected() await connected_a01_mqtt_client.async_disconnect() assert not connected_a01_mqtt_client.is_connected() # Broker received a connect and subscribe. Disconnect packet is not # guaranteed to be captured by the time the async_disconnect returns assert received_requests.qsize() >= 2 # Connect and Subscribe async def test_connect_failure( received_requests: Queue, response_queue: Queue, a01_mqtt_client: RoborockMqttClientA01 ) -> None: """Test the broker responding with a connect failure.""" response_queue.put(mqtt_packet.gen_connack(rc=1)) with pytest.raises(RoborockException, match="Failed to connect"): await a01_mqtt_client.async_connect() assert not a01_mqtt_client.is_connected() assert received_requests.qsize() == 1 # Connect attempt async def test_disconnect_already_disconnected(connected_a01_mqtt_client: RoborockMqttClientA01) -> None: """Test the MQTT client error handling for a no-op disconnect.""" assert connected_a01_mqtt_client.is_connected() # Make the MQTT client simulate returning that it already thinks it is disconnected with patch("roborock.cloud_api.mqtt.Client.disconnect", return_value=mqtt.MQTT_ERR_NO_CONN): await connected_a01_mqtt_client.async_disconnect() async def test_disconnect_failure(connected_a01_mqtt_client: RoborockMqttClientA01) -> None: """Test that the MQTT client ignores MQTT client error handling for a no-op disconnect.""" assert connected_a01_mqtt_client.is_connected() # Make the MQTT client returns with an error when disconnecting with patch("roborock.cloud_api.mqtt.Client.disconnect", return_value=mqtt.MQTT_ERR_PROTOCOL), pytest.raises( RoborockException, match="Failed to disconnect" ): await connected_a01_mqtt_client.async_disconnect() async def test_async_release(connected_a01_mqtt_client: RoborockMqttClientA01) -> None: """Test the async_release API will disconnect the client.""" await connected_a01_mqtt_client.async_release() assert not connected_a01_mqtt_client.is_connected() async def test_subscribe_failure( received_requests: Queue, response_queue: Queue, a01_mqtt_client: RoborockMqttClientA01 ) -> None: """Test the broker responding with the wrong message type on subscribe.""" response_queue.put(mqtt_packet.gen_connack(rc=0, flags=2)) with patch("roborock.cloud_api.mqtt.Client.subscribe", return_value=(mqtt.MQTT_ERR_NO_CONN, None)), pytest.raises( RoborockException, match="Failed to subscribe" ): await a01_mqtt_client.async_connect() assert received_requests.qsize() == 1 # Connect attempt # NOTE: The client is "connected" but not "subscribed" and cannot recover # from this state without disconnecting first. This can likely be improved. assert a01_mqtt_client.is_connected() # Attempting to reconnect is a no-op since the client already thinks it is connected await a01_mqtt_client.async_connect() assert a01_mqtt_client.is_connected() assert received_requests.qsize() == 1 def build_rpc_response(message: dict[Any, Any]) -> bytes: """Build an encoded RPC response message.""" return MessageParser.build( [ RoborockMessage( protocol=RoborockMessageProtocol.RPC_RESPONSE, payload=pad( json.dumps( { "dps": message, # {10000: json.dumps(message)}, } ).encode(), AES.block_size, ), version=b"A01", seq=2020, ), ], local_key=LOCAL_KEY, ) async def test_update_values( received_requests: Queue, response_queue: Queue, connected_a01_mqtt_client: RoborockMqttClientA01, ) -> None: """Test sending an arbitrary MQTT message and parsing the response.""" message = build_rpc_response( { 203: 6, # spinning } ) response_queue.put(mqtt_packet.gen_publish(MQTT_PUBLISH_TOPIC, payload=message)) data = await connected_a01_mqtt_client.update_values([RoborockZeoProtocol.STATE]) assert data.get(RoborockZeoProtocol.STATE) == "spinning" async def test_publish_failure( connected_a01_mqtt_client: RoborockMqttClientA01, ) -> None: """Test a failure return code when publishing a messaage.""" msg = mqtt.MQTTMessageInfo(0) msg.rc = mqtt.MQTT_ERR_PROTOCOL with patch("roborock.cloud_api.mqtt.Client.publish", return_value=msg), pytest.raises( RoborockException, match="Failed to publish" ): await connected_a01_mqtt_client.update_values([RoborockZeoProtocol.STATE]) async def test_future_timeout( connected_a01_mqtt_client: RoborockMqttClientA01, ) -> None: """Test a timeout raised while waiting for an RPC response.""" with patch("roborock.roborock_future.async_timeout.timeout", side_effect=asyncio.TimeoutError): data = await connected_a01_mqtt_client.update_values([RoborockZeoProtocol.STATE]) assert data.get(RoborockZeoProtocol.STATE) is None python-roborock-2.19.0/tests/test_api.py000066400000000000000000000275271501065527500203110ustar00rootroot00000000000000import asyncio import json import logging from collections.abc import AsyncGenerator from queue import Queue from typing import Any from unittest.mock import AsyncMock, patch import paho.mqtt.client as mqtt import pytest from roborock import ( HomeData, RoborockDockDustCollectionModeCode, RoborockDockTypeCode, RoborockDockWashTowelModeCode, UserData, ) from roborock.containers import DeviceData, RoomMapping, S7MaxVStatus from roborock.exceptions import RoborockException, RoborockTimeout from roborock.protocol import MessageParser from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol from roborock.version_1_apis import RoborockMqttClientV1 from roborock.web_api import PreparedRequest, RoborockApiClient from tests.mock_data import ( BASE_URL_REQUEST, GET_CODE_RESPONSE, HOME_DATA_RAW, LOCAL_KEY, MQTT_PUBLISH_TOPIC, STATUS, USER_DATA, ) from . import mqtt_packet def test_can_create_prepared_request(): PreparedRequest("https://sample.com", AsyncMock()) async def test_can_create_mqtt_roborock(): home_data = HomeData.from_dict(HOME_DATA_RAW) device_info = DeviceData(device=home_data.devices[0], model=home_data.products[0].model) RoborockMqttClientV1(UserData.from_dict(USER_DATA), device_info) async def test_get_base_url_no_url(): rc = RoborockApiClient("sample@gmail.com") with patch("roborock.web_api.PreparedRequest.request") as mock_request: mock_request.return_value = BASE_URL_REQUEST await rc._get_base_url() assert rc.base_url == "https://sample.com" async def test_request_code(): rc = RoborockApiClient("sample@gmail.com") with patch("roborock.web_api.RoborockApiClient._get_base_url"), patch( "roborock.web_api.RoborockApiClient._get_header_client_id" ), patch("roborock.web_api.PreparedRequest.request") as mock_request: mock_request.return_value = GET_CODE_RESPONSE await rc.request_code() async def test_get_home_data(): rc = RoborockApiClient("sample@gmail.com") with patch("roborock.web_api.RoborockApiClient._get_base_url"), patch( "roborock.web_api.RoborockApiClient._get_header_client_id" ), patch("roborock.web_api.PreparedRequest.request") as mock_prepared_request: mock_prepared_request.side_effect = [ {"code": 200, "msg": "success", "data": {"rrHomeId": 1}}, {"code": 200, "success": True, "result": HOME_DATA_RAW}, ] user_data = UserData.from_dict(USER_DATA) result = await rc.get_home_data(user_data) assert result == HomeData.from_dict(HOME_DATA_RAW) async def test_get_dust_collection_mode(): home_data = HomeData.from_dict(HOME_DATA_RAW) device_info = DeviceData(device=home_data.devices[0], model=home_data.products[0].model) rmc = RoborockMqttClientV1(UserData.from_dict(USER_DATA), device_info) with patch("roborock.version_1_apis.roborock_client_v1.AttributeCache.async_value") as command: command.return_value = {"mode": 1} dust = await rmc.get_dust_collection_mode() assert dust is not None assert dust.mode == RoborockDockDustCollectionModeCode.light async def test_get_mop_wash_mode(): home_data = HomeData.from_dict(HOME_DATA_RAW) device_info = DeviceData(device=home_data.devices[0], model=home_data.products[0].model) rmc = RoborockMqttClientV1(UserData.from_dict(USER_DATA), device_info) with patch("roborock.version_1_apis.roborock_client_v1.AttributeCache.async_value") as command: command.return_value = {"smart_wash": 0, "wash_interval": 1500} mop_wash = await rmc.get_smart_wash_params() assert mop_wash is not None assert mop_wash.smart_wash == 0 assert mop_wash.wash_interval == 1500 async def test_get_washing_mode(): home_data = HomeData.from_dict(HOME_DATA_RAW) device_info = DeviceData(device=home_data.devices[0], model=home_data.products[0].model) rmc = RoborockMqttClientV1(UserData.from_dict(USER_DATA), device_info) with patch("roborock.version_1_apis.roborock_client_v1.AttributeCache.async_value") as command: command.return_value = {"wash_mode": 2} washing_mode = await rmc.get_wash_towel_mode() assert washing_mode is not None assert washing_mode.wash_mode == RoborockDockWashTowelModeCode.deep assert washing_mode.wash_mode == 2 async def test_get_prop(): home_data = HomeData.from_dict(HOME_DATA_RAW) device_info = DeviceData(device=home_data.devices[0], model=home_data.products[0].model) rmc = RoborockMqttClientV1(UserData.from_dict(USER_DATA), device_info) with patch("roborock.version_1_apis.roborock_mqtt_client_v1.RoborockMqttClientV1.get_status") as get_status, patch( "roborock.version_1_apis.roborock_client_v1.RoborockClientV1.send_command" ), patch("roborock.version_1_apis.roborock_client_v1.AttributeCache.async_value"), patch( "roborock.version_1_apis.roborock_mqtt_client_v1.RoborockMqttClientV1.get_dust_collection_mode" ): status = S7MaxVStatus.from_dict(STATUS) status.dock_type = RoborockDockTypeCode.auto_empty_dock_pure get_status.return_value = status props = await rmc.get_prop() assert props assert props.dock_summary assert props.dock_summary.wash_towel_mode is None assert props.dock_summary.smart_wash_params is None assert props.dock_summary.dust_collection_mode is not None @pytest.fixture(name="connected_mqtt_client") async def connected_mqtt_client_fixture( response_queue: Queue, mqtt_client: RoborockMqttClientV1 ) -> AsyncGenerator[RoborockMqttClientV1, None]: response_queue.put(mqtt_packet.gen_connack(rc=0, flags=2)) response_queue.put(mqtt_packet.gen_suback(1, 0)) await mqtt_client.async_connect() yield mqtt_client async def test_async_connect(received_requests: Queue, connected_mqtt_client: RoborockMqttClientV1) -> None: """Test connecting to the MQTT broker.""" assert connected_mqtt_client.is_connected() # Connecting again is a no-op await connected_mqtt_client.async_connect() assert connected_mqtt_client.is_connected() await connected_mqtt_client.async_disconnect() assert not connected_mqtt_client.is_connected() # Broker received a connect and subscribe. Disconnect packet is not # guaranteed to be captured by the time the async_disconnect returns assert received_requests.qsize() >= 2 # Connect and Subscribe async def test_connect_failure_response( received_requests: Queue, response_queue: Queue, mqtt_client: RoborockMqttClientV1 ) -> None: """Test the broker responding with a connect failure.""" response_queue.put(mqtt_packet.gen_connack(rc=1)) with pytest.raises(RoborockException, match="Failed to connect"): await mqtt_client.async_connect() assert not mqtt_client.is_connected() assert received_requests.qsize() == 1 # Connect attempt async def test_disconnect_already_disconnected(connected_mqtt_client: RoborockMqttClientV1) -> None: """Test the MQTT client error handling for a no-op disconnect.""" assert connected_mqtt_client.is_connected() # Make the MQTT client simulate returning that it already thinks it is disconnected with patch("roborock.cloud_api.mqtt.Client.disconnect", return_value=mqtt.MQTT_ERR_NO_CONN): await connected_mqtt_client.async_disconnect() async def test_disconnect_failure(connected_mqtt_client: RoborockMqttClientV1) -> None: """Test that the MQTT client ignores MQTT client error handling for a no-op disconnect.""" assert connected_mqtt_client.is_connected() # Make the MQTT client returns with an error when disconnecting with patch("roborock.cloud_api.mqtt.Client.disconnect", return_value=mqtt.MQTT_ERR_PROTOCOL), pytest.raises( RoborockException, match="Failed to disconnect" ): await connected_mqtt_client.async_disconnect() async def test_disconnect_failure_response( received_requests: Queue, response_queue: Queue, connected_mqtt_client: RoborockMqttClientV1, caplog: pytest.LogCaptureFixture, ) -> None: """Test the broker responding with a connect failure.""" # Enqueue a failed message -- however, the client does not process any # further messages and there is no parsing error, and no failed log messages. response_queue.put(mqtt_packet.gen_disconnect(reason_code=1)) assert connected_mqtt_client.is_connected() with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): await connected_mqtt_client.async_disconnect() assert not connected_mqtt_client.is_connected() assert not caplog.records async def test_async_release(connected_mqtt_client: RoborockMqttClientV1) -> None: """Test the async_release API will disconnect the client.""" await connected_mqtt_client.async_release() assert not connected_mqtt_client.is_connected() async def test_subscribe_failure( received_requests: Queue, response_queue: Queue, mqtt_client: RoborockMqttClientV1 ) -> None: """Test the broker responding with the wrong message type on subscribe.""" response_queue.put(mqtt_packet.gen_connack(rc=0, flags=2)) with patch("roborock.cloud_api.mqtt.Client.subscribe", return_value=(mqtt.MQTT_ERR_NO_CONN, None)), pytest.raises( RoborockException, match="Failed to subscribe" ): await mqtt_client.async_connect() assert received_requests.qsize() == 1 # Connect attempt # NOTE: The client is "connected" but not "subscribed" and cannot recover # from this state without disconnecting first. This can likely be improved. assert mqtt_client.is_connected() # Attempting to reconnect is a no-op since the client already thinks it is connected await mqtt_client.async_connect() assert mqtt_client.is_connected() assert received_requests.qsize() == 1 def build_rpc_response(message: dict[str, Any]) -> bytes: """Build an encoded RPC response message.""" return MessageParser.build( [ RoborockMessage( protocol=RoborockMessageProtocol.RPC_RESPONSE, payload=json.dumps( { "dps": {102: json.dumps(message)}, } ).encode(), seq=2020, ), ], local_key=LOCAL_KEY, ) async def test_get_room_mapping( received_requests: Queue, response_queue: Queue, connected_mqtt_client: RoborockMqttClientV1, ) -> None: """Test sending an arbitrary MQTT message and parsing the response.""" test_request_id = 5050 message = build_rpc_response( { "id": test_request_id, "result": [[16, "2362048"], [17, "2362044"]], } ) response_queue.put(mqtt_packet.gen_publish(MQTT_PUBLISH_TOPIC, payload=message)) with patch("roborock.version_1_apis.roborock_client_v1.get_next_int", return_value=test_request_id): room_mapping = await connected_mqtt_client.get_room_mapping() assert room_mapping == [ RoomMapping(segment_id=16, iot_id="2362048"), RoomMapping(segment_id=17, iot_id="2362044"), ] async def test_publish_failure( connected_mqtt_client: RoborockMqttClientV1, ) -> None: """Test a failure return code when publishing a messaage.""" msg = mqtt.MQTTMessageInfo(0) msg.rc = mqtt.MQTT_ERR_PROTOCOL with patch("roborock.cloud_api.mqtt.Client.publish", return_value=msg), pytest.raises( RoborockException, match="Failed to publish" ): await connected_mqtt_client.get_room_mapping() async def test_future_timeout( connected_mqtt_client: RoborockMqttClientV1, ) -> None: """Test a timeout raised while waiting for an RPC response.""" with patch("roborock.roborock_future.async_timeout.timeout", side_effect=asyncio.TimeoutError), pytest.raises( RoborockTimeout, match="Timeout after" ): await connected_mqtt_client.get_room_mapping() python-roborock-2.19.0/tests/test_containers.py000066400000000000000000000155251501065527500217000ustar00rootroot00000000000000from roborock import CleanRecord, CleanSummary, Consumable, DnDTimer, HomeData, S7MaxVStatus, UserData from roborock.code_mappings import ( RoborockCategory, RoborockDockErrorCode, RoborockDockTypeCode, RoborockErrorCode, RoborockFanSpeedS7MaxV, RoborockMopIntensityS7, RoborockMopModeS7, RoborockStateCode, ) from .mock_data import ( CLEAN_RECORD, CLEAN_SUMMARY, CONSUMABLE, DND_TIMER, HOME_DATA_RAW, LOCAL_KEY, PRODUCT_ID, STATUS, USER_DATA, ) def test_user_data(): ud = UserData.from_dict(USER_DATA) assert ud.uid == 123456 assert ud.tokentype == "token_type" assert ud.token == "abc123" assert ud.rruid == "abc123" assert ud.region == "us" assert ud.country == "US" assert ud.countrycode == "1" assert ud.nickname == "user_nickname" assert ud.rriot.u == "user123" assert ud.rriot.s == "pass123" assert ud.rriot.h == "unknown123" assert ud.rriot.k == "domain123" assert ud.rriot.r.r == "US" assert ud.rriot.r.a == "https://api-us.roborock.com" assert ud.rriot.r.m == "tcp://mqtt-us.roborock.com:8883" assert ud.rriot.r.l == "https://wood-us.roborock.com" assert ud.tuya_device_state == 2 assert ud.avatarurl == "https://files.roborock.com/iottest/default_avatar.png" def test_home_data(): hd = HomeData.from_dict(HOME_DATA_RAW) assert hd.id == 123456 assert hd.name == "My Home" assert hd.lon is None assert hd.lat is None assert hd.geo_name is None product = hd.products[0] assert product.id == PRODUCT_ID assert product.name == "Roborock S7 MaxV" assert product.code == "a27" assert product.model == "roborock.vacuum.a27" assert product.icon_url is None assert product.attribute is None assert product.capability == 0 assert product.category == RoborockCategory.VACUUM schema = product.schema assert schema[0].id == "101" assert schema[0].name == "rpc_request" assert schema[0].code == "rpc_request_code" assert schema[0].mode == "rw" assert schema[0].type == "RAW" assert schema[0].product_property is None assert schema[0].desc is None device = hd.devices[0] assert device.duid == "abc123" assert device.name == "Roborock S7 MaxV" assert device.attribute is None assert device.active_time == 1672364449 assert device.local_key == LOCAL_KEY assert device.runtime_env is None assert device.time_zone_id == "America/Los_Angeles" assert device.icon_url == "no_url" assert device.product_id == "product123" assert device.lon is None assert device.lat is None assert not device.share assert device.share_time is None assert device.online assert device.fv == "02.56.02" assert device.pv == "1.0" assert device.room_id == 2362003 assert device.tuya_uuid is None assert not device.tuya_migrated assert device.extra == '{"RRPhotoPrivacyVersion": "1"}' assert device.sn == "abc123" assert device.feature_set == "2234201184108543" assert device.new_feature_set == "0000000000002041" # status = device.device_status # assert status.name == assert device.silent_ota_switch assert hd.rooms[0].id == 2362048 assert hd.rooms[0].name == "Example room 1" def test_serialize_and_unserialize(): ud = UserData.from_dict(USER_DATA) ud_dict = ud.as_dict() assert ud_dict == USER_DATA def test_consumable(): c = Consumable.from_dict(CONSUMABLE) assert c.main_brush_work_time == 74382 assert c.side_brush_work_time == 74383 assert c.filter_work_time == 74384 assert c.filter_element_work_time == 0 assert c.sensor_dirty_time == 74385 assert c.strainer_work_times == 65 assert c.dust_collection_work_times == 25 assert c.cleaning_brush_work_times == 66 def test_status(): s = S7MaxVStatus.from_dict(STATUS) assert s.msg_ver == 2 assert s.msg_seq == 458 assert s.state == RoborockStateCode.charging assert s.battery == 100 assert s.clean_time == 1176 assert s.clean_area == 20965000 assert s.square_meter_clean_area == 21.0 assert s.error_code == RoborockErrorCode.none assert s.map_present == 1 assert s.in_cleaning == 0 assert s.in_returning == 0 assert s.in_fresh_state == 1 assert s.lab_status == 1 assert s.water_box_status == 1 assert s.back_type == -1 assert s.wash_phase == 0 assert s.wash_ready == 0 assert s.fan_power == 102 assert s.dnd_enabled == 0 assert s.map_status == 3 assert s.is_locating == 0 assert s.lock_status == 0 assert s.water_box_mode == 203 assert s.water_box_carriage_status == 1 assert s.mop_forbidden_enable == 1 assert s.camera_status == 3457 assert s.is_exploring == 0 assert s.home_sec_status == 0 assert s.home_sec_enable_password == 0 assert s.adbumper_status == [0, 0, 0] assert s.water_shortage_status == 0 assert s.dock_type == RoborockDockTypeCode.empty_wash_fill_dock assert s.dust_collection_status == 0 assert s.auto_dust_collection == 1 assert s.avoid_count == 19 assert s.mop_mode == 300 assert s.debug_mode == 0 assert s.collision_avoid_status == 1 assert s.switch_map_mode == 0 assert s.dock_error_status == RoborockDockErrorCode.ok assert s.charge_status == 1 assert s.unsave_map_reason == 0 assert s.unsave_map_flag == 0 assert s.fan_power == RoborockFanSpeedS7MaxV.balanced assert s.mop_mode == RoborockMopModeS7.standard assert s.water_box_mode == RoborockMopIntensityS7.intense def test_dnd_timer(): dnd = DnDTimer.from_dict(DND_TIMER) assert dnd.start_hour == 22 assert dnd.start_minute == 0 assert dnd.end_hour == 7 assert dnd.end_minute == 0 assert dnd.enabled == 1 def test_clean_summary(): cs = CleanSummary.from_dict(CLEAN_SUMMARY) assert cs.clean_time == 74382 assert cs.clean_area == 1159182500 assert cs.square_meter_clean_area == 1159.2 assert cs.clean_count == 31 assert cs.dust_collection_count == 25 assert len(cs.records) == 2 assert cs.records[1] == 1672458041 def test_clean_record(): cr = CleanRecord.from_dict(CLEAN_RECORD) assert cr.begin == 1672543330 assert cr.end == 1672544638 assert cr.duration == 1176 assert cr.area == 20965000 assert cr.square_meter_area == 21.0 assert cr.error == 0 assert cr.complete == 1 assert cr.start_type == 2 assert cr.clean_type == 3 assert cr.finish_reason == 56 assert cr.dust_collection_status == 1 assert cr.avoid_count == 19 assert cr.wash_count == 2 assert cr.map_flag == 0 def test_no_value(): modified_status = STATUS.copy() modified_status["dock_type"] = 9999 s = S7MaxVStatus.from_dict(modified_status) assert s.dock_type == RoborockDockTypeCode.unknown assert -9999 not in RoborockDockTypeCode.keys() assert "missing" not in RoborockDockTypeCode.values() python-roborock-2.19.0/tests/test_local_api_v1.py000066400000000000000000000056771501065527500220730ustar00rootroot00000000000000"""Tests for the Roborock Local Client V1.""" import json from collections.abc import AsyncGenerator from queue import Queue from typing import Any from unittest.mock import patch import pytest from roborock.containers import RoomMapping from roborock.protocol import MessageParser from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol from roborock.version_1_apis import RoborockLocalClientV1 from .mock_data import LOCAL_KEY def build_rpc_response(seq: int, message: dict[str, Any]) -> bytes: """Build an encoded RPC response message.""" return build_raw_response( protocol=RoborockMessageProtocol.GENERAL_REQUEST, seq=seq, payload=json.dumps( { "dps": {102: json.dumps(message)}, } ).encode(), ) def build_raw_response(protocol: RoborockMessageProtocol, seq: int, payload: bytes) -> bytes: """Build an encoded RPC response message.""" message = RoborockMessage( protocol=protocol, random=23, seq=seq, payload=payload, ) return MessageParser.build(message, local_key=LOCAL_KEY) async def test_async_connect( local_client: RoborockLocalClientV1, received_requests: Queue, response_queue: Queue, ): """Test that we can connect to the Roborock device.""" response_queue.put(build_raw_response(RoborockMessageProtocol.HELLO_RESPONSE, 1, b"ignored")) response_queue.put(build_raw_response(RoborockMessageProtocol.PING_RESPONSE, 2, b"ignored")) await local_client.async_connect() assert local_client.is_connected() assert received_requests.qsize() == 2 await local_client.async_disconnect() assert not local_client.is_connected() @pytest.fixture(name="connected_local_client") async def connected_local_client_fixture( response_queue: Queue, local_client: RoborockLocalClientV1, ) -> AsyncGenerator[RoborockLocalClientV1, None]: response_queue.put(build_raw_response(RoborockMessageProtocol.HELLO_RESPONSE, 1, b"ignored")) response_queue.put(build_raw_response(RoborockMessageProtocol.PING_RESPONSE, 2, b"ignored")) await local_client.async_connect() yield local_client async def test_get_room_mapping( received_requests: Queue, response_queue: Queue, connected_local_client: RoborockLocalClientV1, ) -> None: """Test sending an arbitrary MQTT message and parsing the response.""" test_request_id = 5050 message = build_rpc_response( seq=test_request_id, message={ "id": test_request_id, "result": [[16, "2362048"], [17, "2362044"]], }, ) response_queue.put(message) with patch("roborock.version_1_apis.roborock_client_v1.get_next_int", return_value=test_request_id): room_mapping = await connected_local_client.get_room_mapping() assert room_mapping == [ RoomMapping(segment_id=16, iot_id="2362048"), RoomMapping(segment_id=17, iot_id="2362044"), ] python-roborock-2.19.0/tests/test_queue.py000066400000000000000000000012471501065527500206530ustar00rootroot00000000000000import asyncio import pytest from roborock.exceptions import VacuumError from roborock.roborock_future import RoborockFuture def test_can_create(): RoborockFuture(1) @pytest.mark.asyncio async def test_set_result(): rq = RoborockFuture(1) rq.set_result("test") assert await rq.async_get(1) == "test" @pytest.mark.asyncio async def test_set_exception(): rq = RoborockFuture(1) rq.set_exception(VacuumError("test")) with pytest.raises(VacuumError): assert await rq.async_get(1) @pytest.mark.asyncio async def test_get_timeout(): rq = RoborockFuture(1) with pytest.raises(asyncio.TimeoutError): await rq.async_get(0.01) python-roborock-2.19.0/tests/test_roborock_message.py000066400000000000000000000021541501065527500230510ustar00rootroot00000000000000import json from freezegun import freeze_time from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol def test_roborock_message() -> None: """Test the RoborockMessage class is initialized.""" with freeze_time("2025-01-20T12:00:00"): message1 = RoborockMessage( protocol=RoborockMessageProtocol.RPC_REQUEST, payload=json.dumps({"dps": {"101": json.dumps({"id": 4321})}}).encode(), message_retry=None, ) assert message1.get_request_id() == 4321 with freeze_time("2025-01-20T11:00:00"): # Back in time 1hr to test timestamp message2 = RoborockMessage( protocol=RoborockMessageProtocol.RPC_RESPONSE, payload=json.dumps({"dps": {"94": json.dumps({"id": 444}), "102": json.dumps({"id": 333})}}).encode(), message_retry=None, ) assert message2.get_request_id() == 333 # Ensure the sequence, random numbers, etc are initialized properly assert message1.seq != message2.seq assert message1.random != message2.random assert message1.timestamp > message2.timestamp python-roborock-2.19.0/tests/test_util.py000066400000000000000000000041241501065527500205010ustar00rootroot00000000000000import datetime import pytest from roborock.util import parse_time_to_datetime @pytest.mark.skip def validate(start: datetime.datetime, end: datetime.datetime) -> bool: duration = end - start return duration > datetime.timedelta() # start_date < now < end_date def test_start_date_lower_than_now_lower_than_end_date(): start, end = parse_time_to_datetime( (datetime.datetime.now() - datetime.timedelta(hours=2)).time(), (datetime.datetime.now() - datetime.timedelta(hours=1)).time(), ) assert validate(start, end) # start_date > now > end_date def test_start_date_greater_than_now_greater_tat_end_date(): start, end = parse_time_to_datetime( (datetime.datetime.now() + datetime.timedelta(hours=1)).time(), (datetime.datetime.now() + datetime.timedelta(hours=2)).time(), ) assert validate(start, end) # start_date < now > end_date def test_start_date_lower_than_now_greater_than_end_date(): start, end = parse_time_to_datetime( (datetime.datetime.now() - datetime.timedelta(hours=1)).time(), (datetime.datetime.now() + datetime.timedelta(hours=1)).time(), ) assert validate(start, end) # start_date > now < end_date def test_start_date_greater_than_now_lower_than_end_date(): start, end = parse_time_to_datetime( (datetime.datetime.now() + datetime.timedelta(hours=1)).time(), (datetime.datetime.now() - datetime.timedelta(hours=1)).time(), ) assert validate(start, end) # start_date < end_date < now def test_start_date_lower_than_end_date_lower_than_now(): start, end = parse_time_to_datetime( (datetime.datetime.now() - datetime.timedelta(hours=2)).time(), (datetime.datetime.now() - datetime.timedelta(hours=1)).time(), ) assert validate(start, end) # start_date > end_date > now def test_start_date_greater_than_end_date_greater_than_now(): start, end = parse_time_to_datetime( (datetime.datetime.now() + datetime.timedelta(hours=2)).time(), (datetime.datetime.now() + datetime.timedelta(hours=1)).time(), ) assert validate(start, end) python-roborock-2.19.0/tests/test_web_api.py000066400000000000000000000044001501065527500211270ustar00rootroot00000000000000import aiohttp from roborock import HomeData, HomeDataScene, UserData from roborock.web_api import RoborockApiClient from tests.mock_data import HOME_DATA_RAW, USER_DATA async def test_pass_login_flow() -> None: """Test that we can login with a password and we get back the correct userdata object.""" my_session = aiohttp.ClientSession() api = RoborockApiClient(username="test_user@gmail.com", session=my_session) ud = await api.pass_login("password") assert ud == UserData.from_dict(USER_DATA) assert not my_session.closed async def test_code_login_flow() -> None: """Test that we can login with a code and we get back the correct userdata object.""" api = RoborockApiClient(username="test_user@gmail.com") await api.request_code() ud = await api.code_login(4123) assert ud == UserData.from_dict(USER_DATA) async def test_get_home_data_v2(): """Test a full standard flow where we get the home data to end it off. This matches what HA does""" api = RoborockApiClient(username="test_user@gmail.com") await api.request_code() ud = await api.code_login(4123) hd = await api.get_home_data_v2(ud) assert hd == HomeData.from_dict(HOME_DATA_RAW) async def test_nc_prepare(): """Test adding a device and that nothing breaks""" api = RoborockApiClient(username="test_user@gmail.com") await api.request_code() ud = await api.code_login(4123) prepare = await api.nc_prepare(ud, "America/New_York") new_device = await api.add_device(ud, prepare["s"], prepare["t"]) assert new_device["duid"] == "rand_duid" async def test_get_scenes(): """Test that we can get scenes""" api = RoborockApiClient(username="test_user@gmail.com") ud = await api.pass_login("password") sc = await api.get_scenes(ud, "123456") assert sc == [ HomeDataScene.from_dict( { "id": 1234567, "name": "My plan", } ) ] async def test_execute_scene(mock_rest): """Test that we can execute a scene""" api = RoborockApiClient(username="test_user@gmail.com") ud = await api.pass_login("password") await api.execute_scene(ud, 123456) mock_rest.assert_any_call("https://api-us.roborock.com/user/scene/123456/execute", "post")