From 55e6426129ec70f41a019fd9408b227fb8a03b5a Mon Sep 17 00:00:00 2001 From: Luke Date: Wed, 12 Apr 2023 22:54:15 -0400 Subject: [PATCH] chore: new styling (#35) --- .github/workflows/ci.yml | 144 ++++++++++++++-------------- .pre-commit-config.yaml | 32 ++++++- CHANGELOG.md | 26 ++++-- README.md | 176 +++++++++++++++++------------------ pyproject.toml | 15 +++ roborock/__init__.py | 5 +- roborock/api.py | 120 ++++++++++-------------- roborock/cli.py | 11 +-- roborock/cloud_api.py | 62 ++++-------- roborock/code_mappings.py | 10 +- roborock/containers.py | 36 ++++--- roborock/exceptions.py | 11 ++- roborock/local_api.py | 52 +++++------ roborock/roborock_message.py | 82 ++++++++-------- roborock/typing.py | 174 ++++++++++++++++++---------------- tests/conftest.py | 5 +- tests/mock_data.py | 10 +- tests/test_api.py | 33 +++---- tests/test_containers.py | 34 ++++--- tests/test_queue.py | 2 +- 20 files changed, 537 insertions(+), 503 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b818fe..2c16942 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,82 +1,82 @@ name: CI on: - push: - branches: - - main - pull_request: + push: + branches: + - main + pull_request: concurrency: - group: ${{ github.head_ref || github.run_id }} - cancel-in-progress: true + 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@v3 - with: - fetch-depth: 0 - - uses: wagoid/commitlint-github-action@v5.3.0 - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - uses: pre-commit/action@v3.0.0 + # 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@v3 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@v5.3.0 + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + - uses: pre-commit/action@v3.0.0 - test: - strategy: - fail-fast: false - matrix: - python-version: - - "3.10" - - "3.11" - os: - - ubuntu-latest - - windows-latest - - macOS-latest - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - uses: snok/install-poetry@v1.3.3 - - name: Install Dependencies - run: poetry install - shell: bash - - name: Test with Pytest - run: poetry run pytest - shell: bash - release: - runs-on: ubuntu-latest - environment: release - if: github.ref == 'refs/heads/main' - needs: - - test + test: + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.11" + os: + - ubuntu-latest + - windows-latest + - macOS-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - uses: snok/install-poetry@v1.3.3 + - name: Install Dependencies + run: poetry install + shell: bash + - name: Test with Pytest + run: poetry run pytest + shell: bash + release: + runs-on: ubuntu-latest + environment: release + if: github.ref == 'refs/heads/main' + needs: + - test - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - persist-credentials: false + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + persist-credentials: false - # Run semantic release: - # - Update CHANGELOG.md - # - Update version in code - # - Create git tag - # - Create GitHub release - # - Publish to PyPI - - name: Python Semantic Release - uses: relekang/python-semantic-release@v7.33.2 - with: - github_token: ${{ secrets.GH_TOKEN }} - repository_username: __token__ - repository_password: ${{ secrets.PYPI_TOKEN }} + # Run semantic release: + # - Update CHANGELOG.md + # - Update version in code + # - Create git tag + # - Create GitHub release + # - Publish to PyPI + - name: Python Semantic Release + uses: relekang/python-semantic-release@v7.33.2 + with: + github_token: ${{ secrets.GH_TOKEN }} + repository_username: __token__ + repository_password: ${{ secrets.PYPI_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2ac1ee..e17110f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,12 +2,42 @@ # See https://pre-commit.com/hooks.html for more hooks default_stages: [ commit ] - repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.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.3.2 hooks: - id: poetry-check + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: 22.12.0 + hooks: + - id: black + - repo: https://github.com/codespell-project/codespell + rev: v2.2.2 + hooks: + - id: codespell + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.260 + hooks: + - id: ruff + args: + - --fix - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.931 hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index a490b85..92e9acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,22 +3,30 @@ ## v0.6.4 (2023-04-11) + ### Fix -* Disconnect on timeout so next command can work ([`5ad397b`](https://github.com/humbertogontijo/python-roborock/commit/5ad397b3bbb4bc600888baba6c0cc15be9d17ef7)) + +- Disconnect on timeout so next command can work ([`5ad397b`](https://github.com/humbertogontijo/python-roborock/commit/5ad397b3bbb4bc600888baba6c0cc15be9d17ef7)) ## v0.6.3 (2023-04-11) + ### Fix -* Semantic_release ([`63b249d`](https://github.com/humbertogontijo/python-roborock/commit/63b249d65d3fc40b048320e6596aedc40f588bf9)) + +- Semantic_release ([`63b249d`](https://github.com/humbertogontijo/python-roborock/commit/63b249d65d3fc40b048320e6596aedc40f588bf9)) ## v0.6.2 (2023-04-11) + ### Fix -* Error code nogo_zone_detected ([`722e4b5`](https://github.com/humbertogontijo/python-roborock/commit/722e4b5cfd0c4891adc506e9fe99740860027670)) + +- Error code nogo_zone_detected ([`722e4b5`](https://github.com/humbertogontijo/python-roborock/commit/722e4b5cfd0c4891adc506e9fe99740860027670)) ## v0.6.1 (2023-04-10) + ### Fix -* Trigger release ([`f1ce0ed`](https://github.com/humbertogontijo/python-roborock/commit/f1ce0ed55a254bccd8567b48974ff74dd9ec8b25)) -* Trigger release ([`9a4462c`](https://github.com/humbertogontijo/python-roborock/commit/9a4462c800762393cc047085156acbe119cd0fe4)) -* Trigger release ([`b7a664b`](https://github.com/humbertogontijo/python-roborock/commit/b7a664b15b7c5180d816de325537693f47c24860)) -* Trigger release ([`9256849`](https://github.com/humbertogontijo/python-roborock/commit/9256849252f019f4fea2f59384bc0ea7c57adb5c)) -* Lowercase true ([`774c3cc`](https://github.com/humbertogontijo/python-roborock/commit/774c3cc9765ee76a3a553ca6911751124ae7164c)) -* Semantic release not updating changelong ([`eaf6e90`](https://github.com/humbertogontijo/python-roborock/commit/eaf6e90264b6ab69549da0e5bc3d17c4c0a2c07c)) + +- Trigger release ([`f1ce0ed`](https://github.com/humbertogontijo/python-roborock/commit/f1ce0ed55a254bccd8567b48974ff74dd9ec8b25)) +- Trigger release ([`9a4462c`](https://github.com/humbertogontijo/python-roborock/commit/9a4462c800762393cc047085156acbe119cd0fe4)) +- Trigger release ([`b7a664b`](https://github.com/humbertogontijo/python-roborock/commit/b7a664b15b7c5180d816de325537693f47c24860)) +- Trigger release ([`9256849`](https://github.com/humbertogontijo/python-roborock/commit/9256849252f019f4fea2f59384bc0ea7c57adb5c)) +- Lowercase true ([`774c3cc`](https://github.com/humbertogontijo/python-roborock/commit/774c3cc9765ee76a3a553ca6911751124ae7164c)) +- Semantic release not updating changelong ([`eaf6e90`](https://github.com/humbertogontijo/python-roborock/commit/eaf6e90264b6ab69549da0e5bc3d17c4c0a2c07c)) diff --git a/README.md b/README.md index 898a80a..bcbb223 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,86 @@ -# 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 - -This package can encrypt and decrypt the following commands: - -- GET_CLEAN_RECORD -- GET_CONSUMABLE -- GET_MULTI_MAPS_LIST -- APP_START -- APP_PAUSE -- APP_STOP -- APP_CHARGE -- APP_SPOT -- FIND_ME -- RESUME_ZONED_CLEAN -- RESUME_SEGMENT_CLEAN -- SET_CUSTOM_MODE -- SET_MOP_MODE -- SET_WATER_BOX_CUSTOM_MODE -- RESET_CONSUMABLE -- LOAD_MULTI_MAP -- APP_RC_START -- APP_RC_END -- APP_RC_MOVE -- APP_GOTO_TARGET -- APP_SEGMENT_CLEAN -- APP_ZONED_CLEAN -- APP_GET_DRYER_SETTING -- APP_SET_DRYER_SETTING -- APP_START_WASH -- APP_STOP_WASH -- GET_DUST_COLLECTION_MODE -- SET_DUST_COLLECTION_MODE -- GET_SMART_WASH_PARAMS -- SET_SMART_WASH_PARAMS -- GET_WASH_TOWEL_MODE -- SET_WASH_TOWEL_MODE -- SET_CHILD_LOCK_STATUS -- GET_CHILD_LOCK_STATUS -- START_WASH_THEN_CHARGE -- GET_CURRENT_SOUND -- GET_SERIAL_NUMBER -- GET_TIMEZONE -- GET_SERVER_TIMER -- GET_CUSTOMIZE_CLEAN_MODE -- GET_CLEAN_SEQUENCE -- SET_FDS_ENDPOINT -- ENABLE_LOG_UPLOAD -- APP_WAKEUP_ROBOT -- GET_LED_STATUS -- GET_FLOW_LED_STATUS -- SET_FLOW_LED_STATUS -- GET_SOUND_PROGRESS -- GET_SOUND_VOLUME -- TEST_SOUND_VOLUME -- CHANGE_SOUND_VOLUME -- GET_CARPET_MODE -- SET_CARPET_MODE -- GET_CARPET_CLEAN_MODE -- SET_CARPET_CLEAN_MODE -- UPD_SERVER_TIMER -- SET_SERVER_TIMER -- APP_GET_INIT_STATUS -- SET_APP_TIMEZONE -- GET_NETWORK_INFO - - - -## 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 - - +# 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 + +This package can encrypt and decrypt the following commands: + +- GET_CLEAN_RECORD +- GET_CONSUMABLE +- GET_MULTI_MAPS_LIST +- APP_START +- APP_PAUSE +- APP_STOP +- APP_CHARGE +- APP_SPOT +- FIND_ME +- RESUME_ZONED_CLEAN +- RESUME_SEGMENT_CLEAN +- SET_CUSTOM_MODE +- SET_MOP_MODE +- SET_WATER_BOX_CUSTOM_MODE +- RESET_CONSUMABLE +- LOAD_MULTI_MAP +- APP_RC_START +- APP_RC_END +- APP_RC_MOVE +- APP_GOTO_TARGET +- APP_SEGMENT_CLEAN +- APP_ZONED_CLEAN +- APP_GET_DRYER_SETTING +- APP_SET_DRYER_SETTING +- APP_START_WASH +- APP_STOP_WASH +- GET_DUST_COLLECTION_MODE +- SET_DUST_COLLECTION_MODE +- GET_SMART_WASH_PARAMS +- SET_SMART_WASH_PARAMS +- GET_WASH_TOWEL_MODE +- SET_WASH_TOWEL_MODE +- SET_CHILD_LOCK_STATUS +- GET_CHILD_LOCK_STATUS +- START_WASH_THEN_CHARGE +- GET_CURRENT_SOUND +- GET_SERIAL_NUMBER +- GET_TIMEZONE +- GET_SERVER_TIMER +- GET_CUSTOMIZE_CLEAN_MODE +- GET_CLEAN_SEQUENCE +- SET_FDS_ENDPOINT +- ENABLE_LOG_UPLOAD +- APP_WAKEUP_ROBOT +- GET_LED_STATUS +- GET_FLOW_LED_STATUS +- SET_FLOW_LED_STATUS +- GET_SOUND_PROGRESS +- GET_SOUND_VOLUME +- TEST_SOUND_VOLUME +- CHANGE_SOUND_VOLUME +- GET_CARPET_MODE +- SET_CARPET_MODE +- GET_CARPET_CLEAN_MODE +- SET_CARPET_CLEAN_MODE +- UPD_SERVER_TIMER +- SET_SERVER_TIMER +- APP_GET_INIT_STATUS +- SET_APP_TIMEZONE +- GET_NETWORK_INFO + +## 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 diff --git a/pyproject.toml b/pyproject.toml index 3ef7795..9bb9cdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,8 +38,23 @@ pytest-asyncio = "*" pytest = "*" pre-commit = "*" mypy = "*" +ruff = "*" +isort = "*" +black = "*" +codespell = "*" [tool.semantic_release] branch = "main" version_toml = "pyproject.toml:tool.poetry.version" build_command = "pip install poetry && poetry build" + +[tool.ruff] +ignore = ["F403", "E741"] +line-length = 120 + +[tool.black] +line-length = 120 + +[tool.isort] +profile = "black" +line_length = 120 diff --git a/roborock/__init__.py b/roborock/__init__.py index babf188..dbbfbb6 100644 --- a/roborock/__init__.py +++ b/roborock/__init__.py @@ -1,9 +1,6 @@ """Roborock API.""" -from roborock.api import RoborockApiClient -from roborock.cloud_api import RoborockMqttClient -from roborock.local_api import RoborockLocalClient +from roborock.code_mappings import * from roborock.containers import * from roborock.exceptions import * from roborock.typing import * -from roborock.code_mappings import * diff --git a/roborock/api.py b/roborock/api.py index 9e4c7b0..d31fc96 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -14,39 +14,33 @@ import struct import time from random import randint -from typing import Optional, Any, Callable, Coroutine, Mapping +from typing import Any, Callable, Coroutine, Mapping, Optional import aiohttp from Crypto.Cipher import AES from Crypto.Util.Padding import unpad -from roborock.exceptions import ( - RoborockException, RoborockTimeout, VacuumError, -) +from roborock.exceptions import RoborockException, RoborockTimeout, VacuumError + from .code_mappings import RoborockDockTypeCode, RoborockEnum from .containers import ( - UserData, - Status, + CleanRecord, CleanSummary, Consumable, DNDTimer, - CleanRecord, + DustCollectionMode, HomeData, MultiMapsList, - SmartWashParams, + NetworkInfo, RoborockDeviceInfo, + SmartWashParams, + Status, + UserData, WashTowelMode, - DustCollectionMode, - NetworkInfo, - ) from .roborock_message import RoborockMessage from .roborock_queue import RoborockQueue -from .typing import ( - RoborockDeviceProp, - RoborockCommand, - RoborockDockSummary -) +from .typing import RoborockCommand, RoborockDeviceProp, RoborockDockSummary _LOGGER = logging.getLogger(__name__) QUEUE_TIMEOUT = 4 @@ -67,9 +61,7 @@ def __init__(self, base_url: str, base_headers: Optional[dict] = None) -> None: self.base_url = base_url self.base_headers = base_headers or {} - async def request( - self, method: str, url: str, params=None, data=None, headers=None - ) -> dict: + async def request(self, method: str, url: str, params=None, data=None, headers=None) -> dict: _url = "/".join(s.strip("/") for s in [self.base_url, url]) _headers = {**self.base_headers, **(headers or {})} async with aiohttp.ClientSession() as session: @@ -84,7 +76,6 @@ async def request( class RoborockClient: - def __init__(self, endpoint: str, devices_info: Mapping[str, RoborockDeviceInfo]) -> None: self.devices_info = devices_info self._endpoint = endpoint @@ -117,7 +108,8 @@ async def on_message(self, messages: list[RoborockMessage]) -> None: ( None, VacuumError( - error.get("code"), error.get("message") + error.get("code"), + error.get("message"), ), ), timeout=QUEUE_TIMEOUT, @@ -126,18 +118,14 @@ async def on_message(self, messages: list[RoborockMessage]) -> None: result = data_point_response.get("result") if isinstance(result, list) and len(result) == 1: result = result[0] - await queue.async_put( - (result, None), timeout=QUEUE_TIMEOUT - ) + await queue.async_put((result, None), timeout=QUEUE_TIMEOUT) elif protocol == 301: payload = data.payload[0:24] [endpoint, _, request_id, _] = struct.unpack("<15sBH6s", payload) if endpoint.decode().startswith(self._endpoint): iv = bytes(AES.block_size) decipher = AES.new(self._nonce, AES.MODE_CBC, iv) - decrypted = unpad( - decipher.decrypt(data.payload[24:]), AES.block_size - ) + decrypted = unpad(decipher.decrypt(data.payload[24:]), AES.block_size) decrypted = gzip.decompress(decrypted) queue = self._waiting_queue.get(request_id) if queue: @@ -154,15 +142,11 @@ async def _async_response(self, request_id: int, protocol_id: int = 0) -> tuple[ (response, err) = await queue.async_get(QUEUE_TIMEOUT) return response, err except (asyncio.TimeoutError, asyncio.CancelledError): - raise RoborockTimeout( - f"Timeout after {QUEUE_TIMEOUT} seconds waiting for response" - ) from None + raise RoborockTimeout(f"Timeout after {QUEUE_TIMEOUT} seconds waiting for response") from None finally: del self._waiting_queue[request_id] - def _get_payload( - self, method: RoborockCommand, params: Optional[list] = None, secured=False - ): + def _get_payload(self, method: RoborockCommand, params: Optional[list] = None, secured=False): timestamp = math.floor(time.time()) request_id = randint(10000, 99999) inner = { @@ -186,9 +170,7 @@ def _get_payload( ) return request_id, timestamp, payload - async def send_command( - self, device_id: str, method: RoborockCommand, params: Optional[list] = None - ): + async def send_command(self, device_id: str, method: RoborockCommand, params: Optional[list] = None): raise NotImplementedError async def get_status(self, device_id: str) -> Status | None: @@ -208,22 +190,18 @@ async def get_dnd_timer(self, device_id: str) -> DNDTimer | None: async def get_clean_summary(self, device_id: str) -> CleanSummary | None: try: - clean_summary = await self.send_command( - device_id, RoborockCommand.GET_CLEAN_SUMMARY - ) + clean_summary = await self.send_command(device_id, RoborockCommand.GET_CLEAN_SUMMARY) if isinstance(clean_summary, dict): return CleanSummary.from_dict(clean_summary) elif isinstance(clean_summary, bytes): - return CleanSummary(clean_time=int.from_bytes(clean_summary, 'big')) + return CleanSummary(clean_time=int.from_bytes(clean_summary, "big")) except RoborockTimeout as e: _LOGGER.error(e) return None async def get_clean_record(self, device_id: str, record_id: int) -> CleanRecord | None: try: - clean_record = await self.send_command( - device_id, RoborockCommand.GET_CLEAN_RECORD, [record_id] - ) + clean_record = await self.send_command(device_id, RoborockCommand.GET_CLEAN_RECORD, [record_id]) if isinstance(clean_record, dict): return CleanRecord.from_dict(clean_record) except RoborockTimeout as e: @@ -273,18 +251,21 @@ async def get_dock_summary(self, device_id: str, dock_type: RoborockEnum) -> Rob if RoborockDockTypeCode.name != "RoborockDockTypeCode": raise RoborockException("Invalid enum given for dock type") try: - commands: list[Coroutine[Any, Any, DustCollectionMode | WashTowelMode | SmartWashParams | None]] = [ - self.get_dust_collection_mode(device_id)] - if dock_type == RoborockDockTypeCode['3']: - commands += [self.get_wash_towel_mode(device_id), self.get_smart_wash_params(device_id)] - [ - dust_collection_mode, - wash_towel_mode, - smart_wash_params - ] = ( - list(await asyncio.gather(*commands)) - + [None, None] - )[:3] + commands: list[ + Coroutine[ + Any, + Any, + DustCollectionMode | WashTowelMode | SmartWashParams | None, + ] + ] = [self.get_dust_collection_mode(device_id)] + if dock_type == RoborockDockTypeCode["3"]: + commands += [ + self.get_wash_towel_mode(device_id), + self.get_smart_wash_params(device_id), + ] + [dust_collection_mode, wash_towel_mode, smart_wash_params] = ( + list(await asyncio.gather(*commands)) + [None, None] + )[:3] return RoborockDockSummary(dust_collection_mode, wash_towel_mode, smart_wash_params) except RoborockTimeout as e: @@ -302,23 +283,24 @@ async def get_prop(self, device_id: str) -> RoborockDeviceProp | None: ) 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( - device_id, clean_summary.records[0] - ) + last_clean_record = await self.get_clean_record(device_id, clean_summary.records[0]) dock_summary = None - if status and status.dock_type != RoborockDockTypeCode['0']: + if status and status.dock_type != RoborockDockTypeCode["0"]: dock_summary = await self.get_dock_summary(device_id, status.dock_type) if any([status, dnd_timer, clean_summary, consumable]): return RoborockDeviceProp( - status, dnd_timer, clean_summary, consumable, last_clean_record, dock_summary + status, + dnd_timer, + clean_summary, + consumable, + last_clean_record, + dock_summary, ) return None async def get_multi_maps_list(self, device_id) -> MultiMapsList | None: try: - multi_maps_list = await self.send_command( - device_id, RoborockCommand.GET_MULTI_MAPS_LIST - ) + multi_maps_list = await self.send_command(device_id, RoborockCommand.GET_MULTI_MAPS_LIST) if isinstance(multi_maps_list, dict): return MultiMapsList.from_dict(multi_maps_list) except RoborockTimeout as e: @@ -437,9 +419,7 @@ async def get_home_data(self, user_data: UserData) -> HomeData: rriot = user_data.rriot if rriot is None: raise RoborockException("rriot is none") - home_id_request = PreparedRequest( - base_url, {"header_clientid": header_clientid} - ) + home_id_request = PreparedRequest(base_url, {"header_clientid": header_clientid}) home_id_response = await home_id_request.request( "get", "/api/v1/getHomeDetail", @@ -450,7 +430,7 @@ async def get_home_data(self, user_data: UserData) -> HomeData: if home_id_response.get("code") != 200: raise RoborockException(home_id_response.get("msg")) - home_id = home_id_response['data'].get("rrHomeId") + home_id = home_id_response["data"].get("rrHomeId") timestamp = math.floor(time.time()) nonce = secrets.token_urlsafe(6) prestr = ":".join( @@ -464,16 +444,14 @@ async def get_home_data(self, user_data: UserData) -> HomeData: "", ] ) - mac = base64.b64encode( - hmac.new(rriot.h.encode(), prestr.encode(), hashlib.sha256).digest() - ).decode() + mac = base64.b64encode(hmac.new(rriot.h.encode(), prestr.encode(), hashlib.sha256).digest()).decode() if rriot.r.a is None: raise RoborockException("Missing field 'a' in rriot reference") home_request = PreparedRequest( rriot.r.a, { "Authorization": f'Hawk id="{rriot.u}", s="{rriot.s}", ts="{timestamp}", nonce="{nonce}", ' - f'mac="{mac}"', + f'mac="{mac}"', }, ) home_response = await home_request.request("get", "/user/homes/" + str(home_id)) diff --git a/roborock/cli.py b/roborock/cli.py index bd1b9cc..2727a67 100644 --- a/roborock/cli.py +++ b/roborock/cli.py @@ -25,14 +25,14 @@ def __init__(self): def reload(self): if self.roborock_file.is_file(): - with open(self.roborock_file, 'r') as f: + with open(self.roborock_file, "r") 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: + with open(self.roborock_file, "w") as f: f.write(data) self.reload() @@ -50,9 +50,7 @@ def login_data(self): @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_config: Dict[str, Any] = {"level": logging.DEBUG if debug > 0 else logging.INFO} logging.basicConfig(**logging_config) # type: ignore ctx.obj = RoborockContext() @@ -83,7 +81,8 @@ async def _discover(ctx): home_data = await client.get_home_data(login_data.user_data) context.update(LoginData({**login_data, "home_data": home_data})) click.echo( - f"Discovered devices {', '.join([device.name for device in home_data.devices + home_data.received_devices])}") + f"Discovered devices {', '.join([device.name for device in home_data.devices + home_data.received_devices])}" + ) @click.command() diff --git a/roborock/cloud_api.py b/roborock/cloud_api.py index 69a44be..6ee074c 100644 --- a/roborock/cloud_api.py +++ b/roborock/cloud_api.py @@ -5,22 +5,16 @@ import threading import uuid from asyncio import Lock -from typing import Optional, Any, Mapping +from typing import Any, Mapping, Optional from urllib.parse import urlparse import paho.mqtt.client as mqtt -from roborock.api import md5hex, RoborockClient, SPECIAL_COMMANDS -from roborock.exceptions import ( - RoborockException, - CommandVacuumError, - VacuumError, -) -from .containers import ( - UserData, - RoborockDeviceInfo, -) -from .roborock_message import RoborockParser, md5bin, RoborockMessage +from roborock.api import SPECIAL_COMMANDS, RoborockClient, md5hex +from roborock.exceptions import CommandVacuumError, RoborockException, VacuumError + +from .containers import RoborockDeviceInfo, UserData +from .roborock_message import RoborockMessage, RoborockParser, md5bin from .roborock_queue import RoborockQueue from .typing import RoborockCommand from .util import run_in_executor @@ -70,9 +64,7 @@ async def on_connect(self, _client, _, __, rc, ___=None) -> None: message = f"Failed to connect (rc: {rc})" _LOGGER.error(message) if connection_queue: - await connection_queue.async_put( - (None, VacuumError(rc, message)), timeout=QUEUE_TIMEOUT - ) + await connection_queue.async_put((None, VacuumError(rc, message)), timeout=QUEUE_TIMEOUT) return _LOGGER.info(f"Connected to mqtt {self._mqtt_host}:{self._mqtt_port}") topic = f"rr/m/o/{self._mqtt_user}/{self._hashed_user}/#" @@ -81,9 +73,7 @@ async def on_connect(self, _client, _, __, rc, ___=None) -> None: message = f"Failed to subscribe (rc: {result})" _LOGGER.error(message) if connection_queue: - await connection_queue.async_put( - (None, VacuumError(rc, message)), timeout=QUEUE_TIMEOUT - ) + await connection_queue.async_put((None, VacuumError(rc, message)), timeout=QUEUE_TIMEOUT) return _LOGGER.info(f"Subscribed to topic {topic}") if connection_queue: @@ -107,9 +97,7 @@ async def on_disconnect(self, _client: mqtt.Client, _, rc, __=None) -> None: _LOGGER.warning(message) connection_queue = self._waiting_queue.get(1) if connection_queue: - await connection_queue.async_put( - (True, None), timeout=QUEUE_TIMEOUT - ) + await connection_queue.async_put((True, None), timeout=QUEUE_TIMEOUT) except Exception as ex: _LOGGER.exception(ex) @@ -121,8 +109,10 @@ async def _async_check_keepalive(self) -> None: async with self._mutex: now = mqtt.time_func() # noinspection PyUnresolvedReferences - if now - self._last_disconnection > self._keepalive ** 2 and now - self._last_device_msg_in > self._keepalive: # type: ignore[attr-defined] - + if ( + now - self._last_disconnection > self._keepalive**2 # type: ignore[attr-defined] + and now - self._last_device_msg_in > self._keepalive # type: ignore[attr-defined] + ): self._ping_t = self._last_device_msg_in def _check_keepalive(self) -> None: @@ -146,7 +136,7 @@ def sync_disconnect(self) -> bool: if self.is_connected(): _LOGGER.info("Disconnecting from mqtt") rc = super().disconnect() - if not rc in [mqtt.MQTT_ERR_SUCCESS, mqtt.MQTT_ERR_NO_CONN]: + if rc not in [mqtt.MQTT_ERR_SUCCESS, mqtt.MQTT_ERR_NO_CONN]: raise RoborockException(f"Failed to disconnect (rc:{rc})") return rc == mqtt.MQTT_ERR_SUCCESS @@ -157,11 +147,7 @@ def sync_connect(self) -> bool: if self._mqtt_port is None or self._mqtt_host is None: raise RoborockException("Mqtt information was not entered. Cannot connect.") _LOGGER.info("Connecting to mqtt") - rc = super().connect( - host=self._mqtt_host, - port=self._mqtt_port, - keepalive=MQTT_KEEPALIVE - ) + rc = super().connect(host=self._mqtt_host, port=self._mqtt_port, keepalive=MQTT_KEEPALIVE) if rc != mqtt.MQTT_ERR_SUCCESS: raise RoborockException(f"Failed to connect (rc:{rc})") return rc == mqtt.MQTT_ERR_SUCCESS @@ -188,25 +174,17 @@ async def validate_connection(self) -> None: await self.async_connect() def _send_msg_raw(self, device_id, msg) -> None: - info = self.publish( - f"rr/m/i/{self._mqtt_user}/{self._hashed_user}/{device_id}", msg - ) + info = self.publish(f"rr/m/i/{self._mqtt_user}/{self._hashed_user}/{device_id}", msg) if info.rc != mqtt.MQTT_ERR_SUCCESS: raise RoborockException(f"Failed to publish (rc: {info.rc})") - async def send_command( - self, device_id: str, method: RoborockCommand, params: Optional[list] = None - ): + async def send_command(self, device_id: str, method: RoborockCommand, params: Optional[list] = None): await self.validate_connection() request_id, timestamp, payload = super()._get_payload(method, params, True) _LOGGER.debug(f"id={request_id} Requesting method {method} with {params}") request_protocol = 101 response_protocol = 301 if method in SPECIAL_COMMANDS else 102 - roborock_message = RoborockMessage( - timestamp=timestamp, - protocol=request_protocol, - payload=payload - ) + roborock_message = RoborockMessage(timestamp=timestamp, protocol=request_protocol, payload=payload) local_key = self.devices_info[device_id].device.local_key msg = RoborockParser.encode(roborock_message, local_key) self._send_msg_raw(device_id, msg) @@ -214,9 +192,7 @@ async def send_command( if err: raise CommandVacuumError(method, err) from err if response_protocol == 301: - _LOGGER.debug( - f"id={request_id} Response from {method}: {len(response)} bytes" - ) + _LOGGER.debug(f"id={request_id} Response from {method}: {len(response)} bytes") else: _LOGGER.debug(f"id={request_id} Response from {method}: {response}") return response diff --git a/roborock/code_mappings.py b/roborock/code_mappings.py index 61c5646..36fa706 100644 --- a/roborock/code_mappings.py +++ b/roborock/code_mappings.py @@ -1,16 +1,13 @@ from __future__ import annotations from enum import Enum -from typing import TypeVar, Type, Any +from typing import Any, Type, TypeVar _StrEnumT = TypeVar("_StrEnumT", bound="RoborockEnum") class RoborockEnum(str, Enum): - - def __new__( - cls: Type[_StrEnumT], value: str, *args: Any, **kwargs: Any - ) -> _StrEnumT: + def __new__(cls: Type[_StrEnumT], value: str, *args: Any, **kwargs: Any) -> _StrEnumT: """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") @@ -74,7 +71,8 @@ def create_code_enum(name: str, data: dict) -> RoborockEnum: 26: "going_to_wash_the_mop", # on a46, #1435 100: "charging_complete", 101: "device_offline", - }) + }, +) RoborockErrorCode = create_code_enum( "RoborockErrorCode", diff --git a/roborock/containers.py b/roborock/containers.py index dc0ae18..fa1fe45 100644 --- a/roborock/containers.py +++ b/roborock/containers.py @@ -1,25 +1,35 @@ from __future__ import annotations import re -from dataclasses import dataclass, asdict +from dataclasses import asdict, dataclass from enum import Enum from typing import Any, Optional -from dacite import from_dict, Config +from dacite import Config, from_dict -from roborock.code_mappings import RoborockDockWashTowelModeCode, RoborockDockTypeCode, RoborockMopIntensityCode, \ - RoborockStateCode -from .code_mappings import RoborockMopModeCode, RoborockDockErrorCode, \ - RoborockErrorCode, RoborockDockDustCollectionModeCode, RoborockFanPowerCode +from roborock.code_mappings import ( + RoborockDockTypeCode, + RoborockDockWashTowelModeCode, + RoborockMopIntensityCode, + RoborockStateCode, +) + +from .code_mappings import ( + RoborockDockDustCollectionModeCode, + RoborockDockErrorCode, + RoborockErrorCode, + RoborockFanPowerCode, + RoborockMopModeCode, +) def camelize(s: str): - first, *others = s.split('_') - return ''.join([first.lower(), *map(str.title, others)]) + first, *others = s.split("_") + return "".join([first.lower(), *map(str.title, others)]) def decamelize(s: str): - return re.sub('([A-Z]+)', '_\\1', s).lower() + return re.sub("([A-Z]+)", "_\\1", s).lower() def decamelize_obj(d: dict | list): @@ -30,15 +40,15 @@ def decamelize_obj(d: dict | list): @dataclass class RoborockBase: - @classmethod def from_dict(cls, data: dict[str, Any]): return from_dict(cls, decamelize_obj(data), config=Config(cast=[Enum])) def as_dict(self): - return asdict(self, dict_factory=lambda _fields: { - camelize(key): value for (key, value) in _fields if value is not None - }) + return asdict( + self, + dict_factory=lambda _fields: {camelize(key): value for (key, value) in _fields if value is not None}, + ) @dataclass diff --git a/roborock/exceptions.py b/roborock/exceptions.py index 65aa10c..bcfeaef 100644 --- a/roborock/exceptions.py +++ b/roborock/exceptions.py @@ -1,31 +1,38 @@ """Roborock exceptions.""" + class RoborockException(BaseException): """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.""" + def __init__(self, code, message): self.code = code self.message = message super().__init__() - def __str__(self, *args, **kwargs): # real signature unknown - """ Return str(self). """ + def __str__(self, *args, **kwargs): # real signature unknown + """Return str(self).""" return f"{self.code}: {self.message}" class CommandVacuumError(RoborockException): """Class for command vacuum errors.""" + def __init__(self, command: str, vacuum_error: VacuumError): self.message = f"{command}: {str(vacuum_error)}" super().__init__(self.message) diff --git a/roborock/local_api.py b/roborock/local_api.py index 50ccf24..3d5dea5 100644 --- a/roborock/local_api.py +++ b/roborock/local_api.py @@ -4,22 +4,21 @@ import logging import socket from asyncio import Lock -from typing import Optional, Callable, Awaitable, Any, Mapping +from typing import Any, Awaitable, Callable, Mapping, Optional import async_timeout -from roborock.api import RoborockClient, SPECIAL_COMMANDS +from roborock.api import SPECIAL_COMMANDS, RoborockClient from roborock.containers import RoborockLocalDeviceInfo -from roborock.exceptions import RoborockTimeout, CommandVacuumError, RoborockConnectionException, RoborockException -from roborock.roborock_message import RoborockParser, RoborockMessage -from roborock.typing import RoborockCommand, CommandInfoMap +from roborock.exceptions import CommandVacuumError, RoborockConnectionException, RoborockException, RoborockTimeout +from roborock.roborock_message import RoborockMessage, RoborockParser +from roborock.typing import CommandInfoMap, RoborockCommand from roborock.util import get_running_loop_or_create_one _LOGGER = logging.getLogger(__name__) class RoborockLocalClient(RoborockClient): - def __init__(self, devices_info: Mapping[str, RoborockLocalDeviceInfo]): super().__init__("abc", devices_info) self.loop = get_running_loop_or_create_one() @@ -27,7 +26,7 @@ def __init__(self, devices_info: Mapping[str, RoborockLocalDeviceInfo]): device_id: RoborockSocketListener( device_info.network_info.ip, device_info.device.local_key, - self.on_message + self.on_message, ) for device_id, device_info in devices_info.items() } @@ -36,17 +35,12 @@ def __init__(self, devices_info: Mapping[str, RoborockLocalDeviceInfo]): self._executing = False async def async_connect(self): - await asyncio.gather(*[ - listener.connect() - for listener in self.device_listener.values() - ]) + await asyncio.gather(*[listener.connect() for listener in self.device_listener.values()]) async def async_disconnect(self) -> None: await asyncio.gather(*[listener.disconnect() for listener in self.device_listener.values()]) - def build_roborock_message( - self, method: RoborockCommand, params: Optional[list] = None - ) -> RoborockMessage: + def build_roborock_message(self, method: RoborockCommand, params: Optional[list] = None) -> RoborockMessage: secured = True if method in SPECIAL_COMMANDS else False request_id, timestamp, payload = self._get_payload(method, params, secured) _LOGGER.debug(f"id={request_id} Requesting method {method} with {params}") @@ -62,12 +56,10 @@ def build_roborock_message( prefix=prefix, timestamp=timestamp, protocol=request_protocol, - payload=payload + payload=payload, ) - async def send_command( - self, device_id: str, method: RoborockCommand, params: Optional[list] = None - ): + async def send_command(self, device_id: str, method: RoborockCommand, params: Optional[list] = None): roborock_message = self.build_roborock_message(method, params) response = (await self.send_message(device_id, roborock_message))[0] if isinstance(response, BaseException): @@ -85,9 +77,7 @@ async def async_local_response(self, roborock_message: RoborockMessage): _LOGGER.debug(f"id={request_id} Response from {roborock_message.get_method()}: {response}") return response - async def send_message( - self, device_id: str, roborock_messages: list[RoborockMessage] | RoborockMessage - ): + async def send_message(self, device_id: str, roborock_messages: list[RoborockMessage] | RoborockMessage): if isinstance(roborock_messages, RoborockMessage): roborock_messages = [roborock_messages] local_key = self.devices_info[device_id].device.local_key @@ -101,7 +91,8 @@ async def send_message( return await asyncio.gather( *[self.async_local_response(roborock_message) for roborock_message in roborock_messages], - return_exceptions=True) + return_exceptions=True, + ) class RoborockSocket(socket.socket): @@ -115,8 +106,13 @@ def is_closed(self): class RoborockSocketListener: roborock_port = 58867 - def __init__(self, ip: str, local_key: str, on_message: Callable[[list[RoborockMessage]], Awaitable[Any]], - timeout: float | int = 4): + def __init__( + self, + ip: str, + local_key: str, + on_message: Callable[[list[RoborockMessage]], Awaitable[Any]], + timeout: float | int = 4, + ): self.ip = ip self.local_key = local_key self.socket = RoborockSocket(socket.AF_INET, socket.SOCK_STREAM) @@ -126,7 +122,7 @@ def __init__(self, ip: str, local_key: str, on_message: Callable[[list[RoborockM self.timeout = timeout self.is_connected = False self._mutex = Lock() - self.remaining = b'' + self.remaining = b"" async def _main_coro(self): while not self.socket.is_closed: @@ -135,7 +131,7 @@ async def _main_coro(self): try: if self.remaining: message = self.remaining + message - self.remaining = b'' + self.remaining = b"" (parser_msg, remaining) = RoborockParser.decode(message, self.local_key) self.remaining = remaining await self.on_message(parser_msg) @@ -172,9 +168,7 @@ async def send_message(self, data: bytes) -> None: await self.loop.sock_sendall(self.socket, data) except (asyncio.TimeoutError, asyncio.CancelledError): await self.disconnect() - raise RoborockTimeout( - f"Timeout after {self.timeout} seconds waiting for response" - ) from None + raise RoborockTimeout(f"Timeout after {self.timeout} seconds waiting for response") from None except BrokenPipeError as e: _LOGGER.exception(e) await self.disconnect() diff --git a/roborock/roborock_message.py b/roborock/roborock_message.py index 544d39e..6a84dc6 100644 --- a/roborock/roborock_message.py +++ b/roborock/roborock_message.py @@ -35,8 +35,8 @@ class RoborockMessage: protocol: int payload: bytes seq: int = randint(100000, 999999) - prefix: bytes = b'' - version: bytes = b'1.0' + prefix: bytes = b"" + version: bytes = b"1.0" random: int = randint(10000, 99999) timestamp: int = math.floor(time.time()) @@ -72,13 +72,12 @@ def get_params(self) -> list | None: class RoborockParser: - @staticmethod def encode(roborock_messages: list[RoborockMessage] | RoborockMessage, local_key: str): if isinstance(roborock_messages, RoborockMessage): roborock_messages = [roborock_messages] - msg = b'' + msg = b"" for roborock_message in roborock_messages: aes_key = md5bin(encode_timestamp(roborock_message.timestamp) + local_key + salt) cipher = AES.new(aes_key, AES.MODE_ECB) @@ -95,62 +94,67 @@ def encode(roborock_messages: list[RoborockMessage] | RoborockMessage, local_key roborock_message.timestamp, roborock_message.protocol, encrypted_len, - encrypted + encrypted, ) if payload: crc32 = binascii.crc32(_msg) _msg += struct.pack("!I", crc32) else: - _msg += b'\x00\x00' + _msg += b"\x00\x00" msg += roborock_message.prefix + _msg return msg @staticmethod def decode(msg: bytes, local_key: str, index=0) -> tuple[list[RoborockMessage], bytes]: - prefix = b'' + prefix = b"" original_index = index if len(msg) - index < 17: ## broken message return [], msg[original_index:] - if msg[index + 4:index + 7] == "1.0".encode(): - prefix = msg[index:index + 4] + if msg[index + 4 : index + 7] == "1.0".encode(): + prefix = msg[index : index + 4] index += 4 - elif msg[index:index + 3] != "1.0".encode(): + elif msg[index : index + 3] != "1.0".encode(): raise RoborockException(f"Unknown protocol version {msg[0:3]!r}") if len(msg) - index in [17]: - [version, request_id, random, timestamp, protocol] = struct.unpack_from( - "!3sIIIH", msg, index - ) - return [RoborockMessage( - prefix=prefix, - version=version, - seq=request_id, - random=random, - timestamp=timestamp, - protocol=protocol, - payload=b'' - )], b'' + [version, request_id, random, timestamp, protocol] = struct.unpack_from("!3sIIIH", msg, index) + return [ + RoborockMessage( + prefix=prefix, + version=version, + seq=request_id, + random=random, + timestamp=timestamp, + protocol=protocol, + payload=b"", + ) + ], b"" if len(msg) - index < 19: ## broken message return [], msg[original_index:] - [version, request_id, random, timestamp, protocol, payload_len] = struct.unpack_from( - "!3sIIIHH", msg, index - ) + [ + version, + request_id, + random, + timestamp, + protocol, + payload_len, + ] = struct.unpack_from("!3sIIIHH", msg, index) index += 19 if payload_len + index + 4 > len(msg): ## broken message return [], msg[original_index:] - payload = b'' + payload = b"" if payload_len == 0: index += 2 else: [payload, expected_crc32] = struct.unpack_from(f"!{payload_len}sI", msg, index) - crc32 = binascii.crc32(msg[index - 19: index + payload_len]) + crc32 = binascii.crc32(msg[index - 19 : index + payload_len]) index += 4 + payload_len if crc32 != expected_crc32: raise RoborockException(f"Wrong CRC32 {crc32}, expected {expected_crc32}") @@ -160,14 +164,16 @@ def decode(msg: bytes, local_key: str, index=0) -> tuple[list[RoborockMessage], decipher = AES.new(aes_key, AES.MODE_ECB) payload = unpad(decipher.decrypt(payload), AES.block_size) - [structs, remaining] = RoborockParser.decode(msg, local_key, index) if index < len(msg) else ([], b'') - - return [RoborockMessage( - prefix=prefix, - version=version, - seq=request_id, - random=random, - timestamp=timestamp, - protocol=protocol, - payload=payload - )] + structs, remaining + [structs, remaining] = RoborockParser.decode(msg, local_key, index) if index < len(msg) else ([], b"") + + return [ + RoborockMessage( + prefix=prefix, + version=version, + seq=request_id, + random=random, + timestamp=timestamp, + protocol=protocol, + payload=payload, + ) + ] + structs, remaining diff --git a/roborock/typing.py b/roborock/typing.py index eaaa16f..5cc20b8 100644 --- a/roborock/typing.py +++ b/roborock/typing.py @@ -4,8 +4,16 @@ from dataclasses import dataclass from enum import Enum -from .containers import Status, CleanSummary, Consumable, \ - DNDTimer, CleanRecord, SmartWashParams, DustCollectionMode, WashTowelMode +from .containers import ( + CleanRecord, + CleanSummary, + Consumable, + DNDTimer, + DustCollectionMode, + SmartWashParams, + Status, + WashTowelMode, +) class RoborockDevicePropField(str, Enum): @@ -117,82 +125,82 @@ class CommandInfo: CommandInfoMap: dict[RoborockCommand, CommandInfo] = { - RoborockCommand.GET_PROP: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_STATUS: CommandInfo(prefix=b'\x00\x00\x00\x77'), - RoborockCommand.SET_CUSTOM_MODE: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_CHILD_LOCK_STATUS: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_MULTI_MAPS_LIST: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_IDENTIFY_FURNITURE_STATUS: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.SET_WATER_BOX_CUSTOM_MODE: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_CLEAN_SEQUENCE: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_CUSTOMIZE_CLEAN_MODE: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_CARPET_CLEAN_MODE: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.SET_CAMERA_STATUS: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.SET_DND_TIMER: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_FLOW_LED_STATUS: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_COLLISION_AVOID_STATUS: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.CLOSE_VALLEY_ELECTRICITY_TIMER: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.SET_FLOW_LED_STATUS: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_VALLEY_ELECTRICITY_TIMER: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_CLEAN_RECORD: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_MAP_V1: CommandInfo(prefix=b'\x00\x00\x00\xc7'), - RoborockCommand.SET_CLEAN_MOTOR_MODE: CommandInfo(prefix=b'\x00\x00\x00\xb7'), - RoborockCommand.GET_CONSUMABLE: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_SERVER_TIMER: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_SERIAL_NUMBER: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_CURRENT_SOUND: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.SET_LED_STATUS: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_CAMERA_STATUS: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_PAUSE: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_CLEAN_SUMMARY: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_NETWORK_INFO: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_LED_STATUS: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.CLOSE_DND_TIMER: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_WAKEUP_ROBOT: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.SET_MOP_MODE: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_DND_TIMER: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_CARPET_MODE: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.GET_TIMEZONE: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.SET_CARPET_MODE: CommandInfo(prefix=b'\x00\x00\x00\xd7'), - RoborockCommand.GET_MULTI_MAP: CommandInfo(prefix=b'\x00\x00\x00\xd7'), - RoborockCommand.SET_COLLISION_AVOID_STATUS: CommandInfo(prefix=b'\x00\x00\x97'), - RoborockCommand.SET_CARPET_CLEAN_MODE: CommandInfo(prefix=b'\x00\x00\x97'), - RoborockCommand.SET_IDENTIFY_GROUND_MATERIAL_STATUS: CommandInfo(prefix=b'\x00\x00\x97'), - RoborockCommand.GET_IDENTIFY_GROUND_MATERIAL_STATUS: CommandInfo(prefix=b'\x00\x00\x97'), - RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER: CommandInfo(prefix=b'\x00\x00\x97'), - RoborockCommand.SWITCH_WATER_MARK: CommandInfo(prefix=b'\x00\x00\x97'), - RoborockCommand.SET_IDENTIFY_FURNITURE_STATUS: CommandInfo(prefix=b'\x00\x00\x97'), - RoborockCommand.SET_CHILD_LOCK_STATUS: CommandInfo(prefix=b'\x00\x00\x97'), - RoborockCommand.GET_CLEAN_RECORD_MAP: CommandInfo(prefix=b'\x00\x00\xe7'), - RoborockCommand.APP_START: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_STOP: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_CHARGE: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_SPOT: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.FIND_ME: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.RESUME_ZONED_CLEAN: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.RESUME_SEGMENT_CLEAN: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.RESET_CONSUMABLE: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.LOAD_MULTI_MAP: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_RC_START: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_RC_END: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_RC_MOVE: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_GOTO_TARGET: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.APP_SEGMENT_CLEAN: CommandInfo(prefix=b'\x00\x00\x00\xc7'), - RoborockCommand.APP_ZONED_CLEAN: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_START_WASH: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.APP_STOP_WASH: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.SET_FDS_ENDPOINT: CommandInfo(prefix=b'\x00\x00\x97'), - RoborockCommand.ENABLE_LOG_UPLOAD: CommandInfo(prefix=b'\x00\x00\x87'), - RoborockCommand.GET_SOUND_VOLUME: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.TEST_SOUND_VOLUME: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.UPD_SERVER_TIMER: CommandInfo(prefix=b'\x00\x00\x00\x97'), - RoborockCommand.SET_APP_TIMEZONE: CommandInfo(prefix=b'\x00\x00\x97'), - RoborockCommand.CHANGE_SOUND_VOLUME: CommandInfo(prefix=b'\x00\x00\x00\x87'), - RoborockCommand.GET_SOUND_PROGRESS: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.SET_SERVER_TIMER: CommandInfo(prefix=b'\x00\x00\x00\xc7'), - RoborockCommand.GET_ROOM_MAPPING: CommandInfo(prefix=b'\x00\x00\x00w'), - RoborockCommand.NAME_SEGMENT: CommandInfo(prefix=b'\x00\x00\x027'), - RoborockCommand.SET_TIMEZONE: CommandInfo(prefix=b'\x00\x00\x00\x97') + RoborockCommand.GET_PROP: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_STATUS: CommandInfo(prefix=b"\x00\x00\x00\x77"), + RoborockCommand.SET_CUSTOM_MODE: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_CHILD_LOCK_STATUS: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_MULTI_MAPS_LIST: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_IDENTIFY_FURNITURE_STATUS: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.SET_WATER_BOX_CUSTOM_MODE: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_CLEAN_SEQUENCE: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_CUSTOMIZE_CLEAN_MODE: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_CARPET_CLEAN_MODE: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.SET_CAMERA_STATUS: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.SET_DND_TIMER: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_FLOW_LED_STATUS: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_COLLISION_AVOID_STATUS: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.CLOSE_VALLEY_ELECTRICITY_TIMER: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.SET_FLOW_LED_STATUS: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_VALLEY_ELECTRICITY_TIMER: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_CLEAN_RECORD: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_MAP_V1: CommandInfo(prefix=b"\x00\x00\x00\xc7"), + RoborockCommand.SET_CLEAN_MOTOR_MODE: CommandInfo(prefix=b"\x00\x00\x00\xb7"), + RoborockCommand.GET_CONSUMABLE: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_SERVER_TIMER: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_SERIAL_NUMBER: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_CURRENT_SOUND: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.SET_LED_STATUS: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_CAMERA_STATUS: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_PAUSE: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_CLEAN_SUMMARY: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_NETWORK_INFO: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_LED_STATUS: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.CLOSE_DND_TIMER: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_WAKEUP_ROBOT: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.SET_MOP_MODE: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_DND_TIMER: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_CARPET_MODE: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.GET_TIMEZONE: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.SET_CARPET_MODE: CommandInfo(prefix=b"\x00\x00\x00\xd7"), + RoborockCommand.GET_MULTI_MAP: CommandInfo(prefix=b"\x00\x00\x00\xd7"), + RoborockCommand.SET_COLLISION_AVOID_STATUS: CommandInfo(prefix=b"\x00\x00\x97"), + RoborockCommand.SET_CARPET_CLEAN_MODE: CommandInfo(prefix=b"\x00\x00\x97"), + RoborockCommand.SET_IDENTIFY_GROUND_MATERIAL_STATUS: CommandInfo(prefix=b"\x00\x00\x97"), + RoborockCommand.GET_IDENTIFY_GROUND_MATERIAL_STATUS: CommandInfo(prefix=b"\x00\x00\x97"), + RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER: CommandInfo(prefix=b"\x00\x00\x97"), + RoborockCommand.SWITCH_WATER_MARK: CommandInfo(prefix=b"\x00\x00\x97"), + RoborockCommand.SET_IDENTIFY_FURNITURE_STATUS: CommandInfo(prefix=b"\x00\x00\x97"), + RoborockCommand.SET_CHILD_LOCK_STATUS: CommandInfo(prefix=b"\x00\x00\x97"), + RoborockCommand.GET_CLEAN_RECORD_MAP: CommandInfo(prefix=b"\x00\x00\xe7"), + RoborockCommand.APP_START: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_STOP: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_CHARGE: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_SPOT: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.FIND_ME: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.RESUME_ZONED_CLEAN: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.RESUME_SEGMENT_CLEAN: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.RESET_CONSUMABLE: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.LOAD_MULTI_MAP: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_RC_START: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_RC_END: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_RC_MOVE: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_GOTO_TARGET: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.APP_SEGMENT_CLEAN: CommandInfo(prefix=b"\x00\x00\x00\xc7"), + RoborockCommand.APP_ZONED_CLEAN: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_START_WASH: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.APP_STOP_WASH: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.SET_FDS_ENDPOINT: CommandInfo(prefix=b"\x00\x00\x97"), + RoborockCommand.ENABLE_LOG_UPLOAD: CommandInfo(prefix=b"\x00\x00\x87"), + RoborockCommand.GET_SOUND_VOLUME: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.TEST_SOUND_VOLUME: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.UPD_SERVER_TIMER: CommandInfo(prefix=b"\x00\x00\x00\x97"), + RoborockCommand.SET_APP_TIMEZONE: CommandInfo(prefix=b"\x00\x00\x97"), + RoborockCommand.CHANGE_SOUND_VOLUME: CommandInfo(prefix=b"\x00\x00\x00\x87"), + RoborockCommand.GET_SOUND_PROGRESS: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.SET_SERVER_TIMER: CommandInfo(prefix=b"\x00\x00\x00\xc7"), + RoborockCommand.GET_ROOM_MAPPING: CommandInfo(prefix=b"\x00\x00\x00w"), + RoborockCommand.NAME_SEGMENT: CommandInfo(prefix=b"\x00\x00\x027"), + RoborockCommand.SET_TIMEZONE: CommandInfo(prefix=b"\x00\x00\x00\x97") # TODO discover prefix for following commands # RoborockCommand.APP_GET_DRYER_SETTING: CommandInfo(prefix=b'\x00\x00\x00w'), # RoborockCommand.APP_SET_DRYER_SETTING: CommandInfo(prefix=b'\x00\x00\x00w'), @@ -207,8 +215,12 @@ class CommandInfo: class RoborockDockSummary: - def __init__(self, dust_collection_mode: DustCollectionMode, - wash_towel_mode: WashTowelMode, smart_wash_params: SmartWashParams) -> None: + def __init__( + self, + dust_collection_mode: DustCollectionMode, + wash_towel_mode: WashTowelMode, + smart_wash_params: SmartWashParams, + ) -> None: self.dust_collection_mode = dust_collection_mode self.wash_towel_mode = wash_towel_mode self.smart_wash_params = smart_wash_params @@ -223,7 +235,7 @@ class RoborockDeviceProp: last_clean_record: typing.Optional[CleanRecord] = None dock_summary: typing.Optional[RoborockDockSummary] = None - def update(self, device_prop: 'RoborockDeviceProp'): + def update(self, device_prop: "RoborockDeviceProp"): if device_prop.status: self.status = device_prop.status if device_prop.dnd_timer: diff --git a/tests/conftest.py b/tests/conftest.py index 557d200..ab90395 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ import pytest -from roborock import RoborockMqttClient, UserData, HomeData -from tests.mock_data import USER_DATA, HOME_DATA_RAW +from roborock import HomeData, UserData +from roborock.cloud_api import RoborockMqttClient +from tests.mock_data import HOME_DATA_RAW, USER_DATA @pytest.fixture(name="mqtt_client") diff --git a/tests/mock_data.py b/tests/mock_data.py index 600fd6e..a1701f8 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -333,11 +333,9 @@ } BASE_URL_REQUEST = { - 'code': 200, - 'msg': 'success', - 'data': { - 'url': 'https://sample.com' - } + "code": 200, + "msg": "success", + "data": {"url": "https://sample.com"}, } -GET_CODE_RESPONSE = {'code': 200, 'msg': 'success', 'data': None} +GET_CODE_RESPONSE = {"code": 200, "msg": "success", "data": None} diff --git a/tests/test_api.py b/tests/test_api.py index 086a5f5..5fa0c85 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -3,11 +3,10 @@ import paho.mqtt.client as mqtt import pytest -from roborock import RoborockApiClient, UserData, HomeData, RoborockDockDustCollectionModeCode, \ - RoborockDockWashTowelModeCode -from roborock.api import PreparedRequest +from roborock import HomeData, RoborockDockDustCollectionModeCode, RoborockDockWashTowelModeCode, UserData +from roborock.api import PreparedRequest, RoborockApiClient from roborock.cloud_api import RoborockMqttClient -from tests.mock_data import BASE_URL_REQUEST, GET_CODE_RESPONSE, USER_DATA, HOME_DATA_RAW +from tests.mock_data import BASE_URL_REQUEST, GET_CODE_RESPONSE, HOME_DATA_RAW, USER_DATA def test_can_create_roborock_client(): @@ -42,9 +41,9 @@ async def test_get_base_url_no_url(): @pytest.mark.asyncio async def test_request_code(): rc = RoborockApiClient("sample@gmail.com") - with patch("roborock.api.RoborockApiClient._get_base_url") as mock_url, patch( - "roborock.api.RoborockApiClient._get_header_client_id") as mock_header_client, patch( - "roborock.api.PreparedRequest.request") as mock_request: + with patch("roborock.api.RoborockApiClient._get_base_url"), patch( + "roborock.api.RoborockApiClient._get_header_client_id" + ), patch("roborock.api.PreparedRequest.request") as mock_request: mock_request.return_value = GET_CODE_RESPONSE await rc.request_code() @@ -52,11 +51,13 @@ async def test_request_code(): @pytest.mark.asyncio async def test_get_home_data(): rc = RoborockApiClient("sample@gmail.com") - with patch("roborock.api.RoborockApiClient._get_base_url") as mock_url, patch( - "roborock.api.RoborockApiClient._get_header_client_id") as mock_header_client, patch( - "roborock.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}] + with patch("roborock.api.RoborockApiClient._get_base_url"), patch( + "roborock.api.RoborockApiClient._get_header_client_id" + ), patch("roborock.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) @@ -73,7 +74,7 @@ async def test_get_dust_collection_mode(): command.return_value = {"mode": 1} dust = await rmc.get_dust_collection_mode(home_data.devices[0].duid) assert dust is not None - assert dust.mode == RoborockDockDustCollectionModeCode['1'] + assert dust.mode == RoborockDockDustCollectionModeCode["1"] @pytest.mark.asyncio @@ -82,7 +83,7 @@ async def test_get_mop_wash_mode(): device_map = {home_data.devices[0].duid: home_data.devices[0]} rmc = RoborockMqttClient(UserData.from_dict(USER_DATA), device_map) with patch("roborock.cloud_api.RoborockMqttClient.send_command") as command: - command.return_value = {'smart_wash': 0, 'wash_interval': 1500} + command.return_value = {"smart_wash": 0, "wash_interval": 1500} mop_wash = await rmc.get_smart_wash_params(home_data.devices[0].duid) assert mop_wash is not None assert mop_wash.smart_wash == 0 @@ -95,7 +96,7 @@ async def test_get_washing_mode(): device_map = {home_data.devices[0].duid: home_data.devices[0]} rmc = RoborockMqttClient(UserData.from_dict(USER_DATA), device_map) with patch("roborock.cloud_api.RoborockMqttClient.send_command") as command: - command.return_value = {'wash_mode': 2} + command.return_value = {"wash_mode": 2} washing_mode = await rmc.get_wash_towel_mode(home_data.devices[0].duid) assert washing_mode is not None - assert washing_mode.wash_mode == RoborockDockWashTowelModeCode['2'] + assert washing_mode.wash_mode == RoborockDockWashTowelModeCode["2"] diff --git a/tests/test_containers.py b/tests/test_containers.py index 0af5cf6..f14fc69 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -1,7 +1,15 @@ -from roborock import UserData, HomeData, Consumable, Status, DNDTimer, CleanSummary, CleanRecord, HomeDataProduct -from roborock.code_mappings import RoborockStateCode, RoborockErrorCode, RoborockFanPowerCode, RoborockMopIntensityCode, \ - RoborockDockTypeCode, RoborockMopModeCode, RoborockDockErrorCode -from .mock_data import USER_DATA, HOME_DATA_RAW, CONSUMABLE, STATUS, DND_TIMER, CLEAN_SUMMARY, CLEAN_RECORD +from roborock import CleanRecord, CleanSummary, Consumable, DNDTimer, HomeData, Status, UserData +from roborock.code_mappings import ( + RoborockDockErrorCode, + RoborockDockTypeCode, + RoborockErrorCode, + RoborockFanPowerCode, + RoborockMopIntensityCode, + RoborockMopModeCode, + RoborockStateCode, +) + +from .mock_data import CLEAN_RECORD, CLEAN_SUMMARY, CONSUMABLE, DND_TIMER, HOME_DATA_RAW, STATUS, USER_DATA def test_user_data(): @@ -12,7 +20,7 @@ def test_user_data(): assert ud.rruid == "abc123" assert ud.region == "us" assert ud.country == "US" - assert ud.countrycode == '1' + assert ud.countrycode == "1" assert ud.nickname == "user_nickname" assert ud.rriot.u == "user123" assert ud.rriot.s == "pass123" @@ -103,11 +111,11 @@ def test_status(): s = Status.from_dict(STATUS) assert s.msg_ver == 2 assert s.msg_seq == 458 - assert s.state == RoborockStateCode['8'] + assert s.state == RoborockStateCode["8"] assert s.battery == 100 assert s.clean_time == 1176 assert s.clean_area == 20965000 - assert s.error_code == RoborockErrorCode['0'] + assert s.error_code == RoborockErrorCode["0"] assert s.map_present == 1 assert s.in_cleaning == 0 assert s.in_returning == 0 @@ -117,12 +125,12 @@ def test_status(): assert s.back_type == -1 assert s.wash_phase == 0 assert s.wash_ready == 0 - assert s.fan_power == RoborockFanPowerCode['102'] + assert s.fan_power == RoborockFanPowerCode["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 == RoborockMopIntensityCode['203'] + assert s.water_box_mode == RoborockMopIntensityCode["203"] assert s.water_box_carriage_status == 1 assert s.mop_forbidden_enable == 1 assert s.camera_status == 3457 @@ -131,15 +139,15 @@ def test_status(): 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['3'] + assert s.dock_type == RoborockDockTypeCode["3"] assert s.dust_collection_status == 0 assert s.auto_dust_collection == 1 assert s.avoid_count == 19 - assert s.mop_mode == RoborockMopModeCode['300'] + assert s.mop_mode == RoborockMopModeCode["300"] assert s.debug_mode == 0 assert s.collision_avoid_status == 1 assert s.switch_map_mode == 0 - assert s.dock_error_status == RoborockDockErrorCode['0'] + assert s.dock_error_status == RoborockDockErrorCode["0"] assert s.charge_status == 1 assert s.unsave_map_reason == 0 assert s.unsave_map_flag == 0 @@ -178,4 +186,4 @@ def test_clean_record(): assert cr.dust_collection_status == 1 assert cr.avoid_count == 19 assert cr.wash_count == 2 - assert cr.map_flag == 0 \ No newline at end of file + assert cr.map_flag == 0 diff --git a/tests/test_queue.py b/tests/test_queue.py index e42d236..8fdfa09 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -20,7 +20,7 @@ async def test_put(): async def test_get_timeout(): rq = RoborockQueue(1) with pytest.raises(asyncio.TimeoutError): - await rq.async_get(.01) + await rq.async_get(0.01) @pytest.mark.asyncio