Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add A01 #199

Merged
merged 8 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
import {
humbertogontijo marked this conversation as resolved.
Show resolved Hide resolved
RuleConfigSeverity,
} from '@commitlint/types';

module.exports = {
extends: ["@commitlint/config-conventional"],
ignores: [(msg) => /Signed-off-by: dependabot\[bot]/m.test(msg)],
rules: {
'type-enum': [
RuleConfigSeverity.Error,
'always',
[
'chore',
'docs',
'feat',
'fix',
'major'
],
]
}
};
12 changes: 12 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,20 @@ pyshark = "^0.6"
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",
"major"
]
major_tags= ["major"]

[tool.ruff]
ignore = ["F403", "E741"]
line-length = 120
select=["E", "F", "UP", "I"]

[tool.ruff.lint.per-file-ignores]
"*/__init__.py" = ["F401"]
3 changes: 3 additions & 0 deletions roborock/version_1_apis/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .roborock_client_v1 import AttributeCache, RoborockClientV1
from .roborock_local_client_v1 import RoborockLocalClientV1
from .roborock_mqtt_client_v1 import RoborockMqttClientV1
2 changes: 2 additions & 0 deletions roborock/version_a01_apis/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .roborock_client_a01 import RoborockClientA01
from .roborock_mqtt_client_a01 import RoborockMqttClientA01
98 changes: 98 additions & 0 deletions roborock/version_a01_apis/roborock_client_a01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import dataclasses
import json
import typing
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,
)
from roborock.containers import DyadProductInfo, DyadSndState
from roborock.roborock_message import (
RoborockDyadDataProtocol,
RoborockMessage,
RoborockMessageProtocol,
)


@dataclasses.dataclass
class DyadProtocolCacheEntry:
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: DyadProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name),
RoborockDyadDataProtocol.SELF_CLEAN_MODE: DyadProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name),
RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: DyadProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name),
RoborockDyadDataProtocol.WARM_LEVEL: DyadProtocolCacheEntry(lambda val: DyadWarmLevel(val).name),
RoborockDyadDataProtocol.CLEAN_MODE: DyadProtocolCacheEntry(lambda val: DyadCleanMode(val).name),
RoborockDyadDataProtocol.SUCTION: DyadProtocolCacheEntry(lambda val: DyadSuction(val).name),
RoborockDyadDataProtocol.WATER_LEVEL: DyadProtocolCacheEntry(lambda val: DyadWaterLevel(val).name),
RoborockDyadDataProtocol.BRUSH_SPEED: DyadProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name),
RoborockDyadDataProtocol.POWER: DyadProtocolCacheEntry(lambda val: int(val)),
RoborockDyadDataProtocol.AUTO_DRY: DyadProtocolCacheEntry(lambda val: bool(val)),
RoborockDyadDataProtocol.MESH_LEFT: DyadProtocolCacheEntry(lambda val: int(360000 - val * 60)),
RoborockDyadDataProtocol.BRUSH_LEFT: DyadProtocolCacheEntry(lambda val: int(360000 - val * 60)),
RoborockDyadDataProtocol.ERROR: DyadProtocolCacheEntry(lambda val: DyadError(val).name),
RoborockDyadDataProtocol.VOLUME_SET: DyadProtocolCacheEntry(lambda val: int(val)),
RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: DyadProtocolCacheEntry(lambda val: bool(val)),
RoborockDyadDataProtocol.AUTO_DRY_MODE: DyadProtocolCacheEntry(lambda val: bool(val)),
RoborockDyadDataProtocol.SILENT_DRY_DURATION: DyadProtocolCacheEntry(lambda val: int(val)), # in minutes
RoborockDyadDataProtocol.SILENT_MODE: DyadProtocolCacheEntry(lambda val: bool(val)),
RoborockDyadDataProtocol.SILENT_MODE_START_TIME: DyadProtocolCacheEntry(
lambda val: time(hour=int(val / 60), minute=val % 60)
), # in minutes since 00:00
RoborockDyadDataProtocol.SILENT_MODE_END_TIME: DyadProtocolCacheEntry(
lambda val: time(hour=int(val / 60), minute=val % 60)
), # in minutes since 00:00
RoborockDyadDataProtocol.RECENT_RUN_TIME: DyadProtocolCacheEntry(
lambda val: [int(v) for v in val.split(",")]
), # minutes of cleaning in past few days.
RoborockDyadDataProtocol.TOTAL_RUN_TIME: DyadProtocolCacheEntry(lambda val: int(val)),
RoborockDyadDataProtocol.SND_STATE: DyadProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)),
RoborockDyadDataProtocol.PRODUCT_INFO: DyadProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)),
}


class RoborockClientA01(RoborockClient):
def __init__(self, endpoint: str, device_info: DeviceData):
super().__init__(endpoint, device_info)

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:
continue
payload_json = json.loads(payload.decode())
for data_point_number, data_point in payload_json.get("dps").items():
data_point_protocol = RoborockDyadDataProtocol(int(data_point_number))
if data_point_protocol in protocol_entries:
converted_response = protocol_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.resolve((converted_response, None))

async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]):
raise NotImplementedError
55 changes: 55 additions & 0 deletions roborock/version_a01_apis/roborock_mqtt_client_a01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import asyncio
import base64
import json

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

from roborock.cloud_api import RoborockMqttClient
from roborock.containers import DeviceData, UserData
from roborock.exceptions import RoborockException
from roborock.protocol import MessageParser, Utils
from roborock.roborock_message import RoborockDyadDataProtocol, RoborockMessage, RoborockMessageProtocol

from .roborock_client_a01 import RoborockClientA01


class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
def __init__(self, user_data: UserData, device_info: DeviceData, queue_timeout: int = 10) -> None:
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, queue_timeout)
RoborockClientA01.__init__(self, endpoint, device_info)

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(asyncio.ensure_future(self._async_response(dps, response_protocol)))
self._send_msg_raw(m)
responses = await asyncio.gather(*futures)
dps_responses = {}
if "10000" in payload["dps"]:
for i, dps in enumerate(json.loads(payload["dps"]["10000"])):
dps_responses[dps] = responses[i][0]
return dps_responses

async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]):
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),
)
)
Loading