Skip to content

Commit 794adfa

Browse files
committed
feat: add support for Roborock Qrevo Curv 2 Flow (a245)
1 parent 3030022 commit 794adfa

90 files changed

Lines changed: 1158 additions & 2898 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pre-commit-config.yaml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
# See https://pre-commit.com for more information
22
# See https://pre-commit.com/hooks.html for more hooks
3-
exclude: >
4-
(?x)^(
5-
CHANGELOG\.md|
6-
roborock/map/proto/.*_pb2\.py
7-
)$
3+
exclude: "CHANGELOG.md"
84
default_stages: [ pre-commit ]
95

106
repos:
@@ -46,7 +42,7 @@ repos:
4642
hooks:
4743
- id: mypy
4844
exclude: cli.py
49-
additional_dependencies: [ "types-paho-mqtt", "types-protobuf" ]
45+
additional_dependencies: [ "types-paho-mqtt" ]
5046
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
5147
rev: v9.23.0
5248
hooks:

CHANGELOG.md

Lines changed: 0 additions & 513 deletions
Large diffs are not rendered by default.

SUPPORTED_FEATURES.md

Lines changed: 194 additions & 193 deletions
Large diffs are not rendered by default.

commitlint.config.mjs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,7 @@ export default {
88
// Disable the rule that enforces lowercase in subject
99
"subject-case": [0], // 0 = disable, 1 = warn, 2 = error
1010
// Disable the rule that enforces a maximum line length in the body
11-
"body-max-line-length": [0, "always"],
12-
// Disable header max length for AI-generated commits
13-
"header-max-length": [0],
14-
// Disable the rule that prevents periods at the end of subjects
15-
"header-full-stop": [0]
11+
"body-max-line-length": [0, "always"]
1612
},
1713

1814
};

device_info.yaml

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,149 @@ roborock.vacuum.a15:
239239
code: offline_status
240240
mode: ro
241241
type: RAW
242+
roborock.vacuum.a245:
243+
protocol_version: '1.0'
244+
product_nickname: VIVIANS
245+
new_feature_info: 4499197267967999
246+
new_feature_info_str: 00000000000200058CC57FFDA86836DD5BBFAF7F7EFEFFFF
247+
feature_info:
248+
- 111
249+
- 112
250+
- 113
251+
- 114
252+
- 115
253+
- 116
254+
- 117
255+
- 118
256+
- 119
257+
- 120
258+
- 121
259+
- 122
260+
- 123
261+
- 124
262+
- 125
263+
product:
264+
id: 64Pgg7cwjxHqT2qQrEh7Ib
265+
name: Qrevo Curv 2 Flow
266+
model: roborock.vacuum.a245
267+
category: robot.vacuum.cleaner
268+
capability: 0
269+
schema:
270+
- id: 101
271+
name: rpc_request
272+
code: rpc_request
273+
mode: rw
274+
type: RAW
275+
- id: 102
276+
name: rpc_response
277+
code: rpc_response
278+
mode: rw
279+
type: RAW
280+
- id: 120
281+
name: "\u9519\u8BEF\u4EE3\u7801"
282+
code: error_code
283+
mode: ro
284+
type: ENUM
285+
property: '{"range": [""]}'
286+
- id: 121
287+
name: "\u8BBE\u5907\u72B6\u6001"
288+
code: state
289+
mode: ro
290+
type: ENUM
291+
property: '{"range": [""]}'
292+
- id: 122
293+
name: "\u8BBE\u5907\u7535\u91CF"
294+
code: battery
295+
mode: ro
296+
type: ENUM
297+
property: '{"range": [""]}'
298+
- id: 123
299+
name: "\u6E05\u626B\u6A21\u5F0F"
300+
code: fan_power
301+
mode: rw
302+
type: ENUM
303+
property: '{"range": [""]}'
304+
- id: 124
305+
name: "\u62D6\u5730\u6A21\u5F0F"
306+
code: water_box_mode
307+
mode: rw
308+
type: ENUM
309+
property: '{"range": [""]}'
310+
- id: 125
311+
name: "\u4E3B\u5237\u5BFF\u547D"
312+
code: main_brush_life
313+
mode: rw
314+
type: VALUE
315+
property: '{"max": 100, "min": 0, "step": 1, "unit": "null", "scale": 1}'
316+
- id: 126
317+
name: "\u8FB9\u5237\u5BFF\u547D"
318+
code: side_brush_life
319+
mode: rw
320+
type: VALUE
321+
property: '{"max": 100, "min": 0, "step": 1, "unit": "null", "scale": 1}'
322+
- id: 127
323+
name: "\u6EE4\u7F51\u5BFF\u547D"
324+
code: filter_life
325+
mode: rw
326+
type: VALUE
327+
property: '{"max": 100, "min": 0, "step": 1, "unit": "null", "scale": 1}'
328+
- id: 128
329+
name: "\u989D\u5916\u72B6\u6001"
330+
code: additional_props
331+
mode: ro
332+
type: RAW
333+
- id: 130
334+
name: "\u5B8C\u6210\u4E8B\u4EF6"
335+
code: task_complete
336+
mode: ro
337+
type: RAW
338+
- id: 131
339+
name: "\u7535\u91CF\u4E0D\u8DB3\u4EFB\u52A1\u53D6\u6D88"
340+
code: task_cancel_low_power
341+
mode: ro
342+
type: RAW
343+
- id: 132
344+
name: "\u8FD0\u52A8\u4E2D\u4EFB\u52A1\u53D6\u6D88"
345+
code: task_cancel_in_motion
346+
mode: ro
347+
type: RAW
348+
- id: 133
349+
name: "\u5145\u7535\u72B6\u6001"
350+
code: charge_status
351+
mode: ro
352+
type: RAW
353+
- id: 134
354+
name: "\u70D8\u5E72\u72B6\u6001"
355+
code: drying_status
356+
mode: ro
357+
type: RAW
358+
- id: 135
359+
name: "\u79BB\u7EBF\u539F\u56E0\u7EC6\u5206"
360+
code: offline_status
361+
mode: ro
362+
type: RAW
363+
- id: 138
364+
name: "\u5DE5\u4F5C\u4EFB\u52A1\u7C7B\u578B"
365+
code: clean_task_type
366+
mode: ro
367+
type: ENUM
368+
property: '{"range": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]}'
369+
- id: 139
370+
name: "\u56DE\u57FA\u7AD9\u76EE\u7684"
371+
code: back_type
372+
mode: ro
373+
type: RAW
374+
- id: 141
375+
name: "\u6E05\u6D01\u8FDB\u5EA6"
376+
code: cleaning_progress
377+
mode: ro
378+
type: VALUE
379+
property: '{"max": 100, "min": 0, "step": 1, "scale": 1}'
380+
- id: 142
381+
name: publish_dsp
382+
code: publish_dsp
383+
mode: ro
384+
type: RAW
242385
roborock.vacuum.a87:
243386
protocol_version: '1.0'
244387
product_nickname: PEARLPLUS

docs/V1_API_COMMANDS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
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.
44

5-
Commands do not immediately make it to this page. You can find more commands [here](https://github.com/Python-roborock/python-roborock/blob/main/roborock/roborock_typing.py#L18)
5+
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)
66

77
Commands can have multiple parameters that can change from one model to another.
88

pyproject.toml

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "python-roborock"
3-
version = "5.4.1"
3+
version = "4.17.2"
44
description = "A package to control Roborock vacuums."
55
authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
66
requires-python = ">=3.11, <4"
@@ -25,15 +25,14 @@ dependencies = [
2525
"pycryptodomex~=3.18 ; sys_platform == 'darwin'",
2626
"paho-mqtt>=1.6.1,<3.0.0",
2727
"construct>=2.10.57,<3",
28-
"protobuf>=6.31.1,<7",
2928
"vacuum-map-parser-roborock",
3029
"pyrate-limiter>=4.0.0,<5",
3130
"aiomqtt>=2.5.0,<3",
3231
"click-shell~=2.1",
3332
]
3433

3534
[project.urls]
36-
Repository = "https://github.com/python-roborock/python-roborock"
35+
Repository = "https://github.com/humbertogontijo/python-roborock"
3736
Documentation = "https://python-roborock.readthedocs.io/"
3837

3938
[project.scripts]
@@ -98,15 +97,9 @@ major_tags= ["refactor"]
9897
lint.ignore = ["F403", "E741"]
9998
lint.select=["E", "F", "UP", "I"]
10099
line-length = 120
101-
extend-exclude = ["roborock/map/proto/*_pb2.py"]
102100

103101
[tool.ruff.lint.per-file-ignores]
104102
"*/__init__.py" = ["F401"]
105-
"roborock/map/proto/*_pb2.py" = ["E501", "I001", "UP009"]
106-
107-
[[tool.mypy.overrides]]
108-
module = ["roborock.map.proto.*"]
109-
ignore_errors = true
110103

111104
[tool.pytest.ini_options]
112105
asyncio_mode = "auto"

roborock/broadcast_protocol.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import asyncio
24
import hashlib
35
import json

roborock/cli.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
from roborock import RoborockCommand
4545
from roborock.data import RoborockBase, UserData
4646
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP, YXCleanType, YXFanLevel
47-
from roborock.data.code_mappings import SHORT_MODEL_TO_ENUM
47+
from roborock.data.code_mappings import SHORT_MODEL_TO_ENUM, RoborockProductNickname
4848
from roborock.device_features import DeviceFeatures
4949
from roborock.devices.cache import Cache, CacheData
5050
from roborock.devices.device import RoborockDevice
@@ -764,6 +764,21 @@ async def network_info(ctx, device_id: str):
764764
await _display_v1_trait(context, device_id, lambda v1: v1.network_info)
765765

766766

767+
def _parse_b01_q10_command(cmd: str) -> B01_Q10_DP:
768+
"""Parse B01_Q10 command from either enum name or value."""
769+
try:
770+
return B01_Q10_DP(int(cmd))
771+
except ValueError:
772+
try:
773+
return B01_Q10_DP.from_name(cmd)
774+
except ValueError:
775+
try:
776+
return B01_Q10_DP.from_value(cmd)
777+
except ValueError:
778+
pass
779+
raise RoborockException(f"Invalid command {cmd} for B01_Q10 device")
780+
781+
767782
@click.command()
768783
@click.option("--device_id", required=True)
769784
@click.option("--cmd", required=True)
@@ -780,8 +795,7 @@ async def command(ctx, cmd, device_id, params):
780795
if result:
781796
click.echo(dump_json(result))
782797
elif device.b01_q10_properties is not None:
783-
if cmd_value := B01_Q10_DP.from_any_optional(cmd) is None:
784-
raise RoborockException(f"Invalid command {cmd} for B01_Q10 device")
798+
cmd_value = _parse_b01_q10_command(cmd)
785799
command_trait: Trait = device.b01_q10_properties.command
786800
await command_trait.send(cmd_value, json.loads(params) if params is not None else None)
787801
click.echo("Command sent successfully; Enable debug logging (-d) to see responses.")
@@ -1066,11 +1080,13 @@ def update_docs(data_file: str, output_file: str):
10661080
# Process the raw data from YAML to build the feature map
10671081
for model, data in product_data_from_yaml.items():
10681082
# Reconstruct the DeviceFeatures object from the raw data in the YAML file
1083+
product_nickname_str = data.get("product_nickname")
1084+
product_nickname = RoborockProductNickname[product_nickname_str] if product_nickname_str else None
10691085
device_features = DeviceFeatures.from_feature_flags(
10701086
new_feature_info=data.get("new_feature_info"),
10711087
new_feature_info_str=data.get("new_feature_info_str"),
10721088
feature_info=data.get("feature_info"),
1073-
product_nickname=data.get("product_nickname"),
1089+
product_nickname=product_nickname,
10741090
)
10751091
features_dict = asdict(device_features)
10761092

@@ -1080,11 +1096,15 @@ def update_docs(data_file: str, output_file: str):
10801096
"protocol_version": data.get("protocol_version", ""),
10811097
"new_feature_info": data.get("new_feature_info", ""),
10821098
"new_feature_info_str": data.get("new_feature_info_str", ""),
1099+
"feature_info": data.get("feature_info", ""),
10831100
}
10841101

10851102
# Populate features from the calculated DeviceFeatures object
10861103
for feature, is_supported in features_dict.items():
10871104
all_feature_names.add(feature)
1105+
if feature in current_product_data:
1106+
# Skip populating the metadata keys as booleans, as they are already set.
1107+
continue
10881108
if is_supported:
10891109
current_product_data[feature] = "X"
10901110

@@ -1106,6 +1126,7 @@ def write_markdown_table(product_features: dict[str, dict[str, any]], all_featur
11061126
"protocol_version",
11071127
"new_feature_info",
11081128
"new_feature_info_str",
1129+
"feature_info",
11091130
]
11101131
# Regular features are the remaining keys, sorted alphabetically
11111132
# We filter out the special rows to avoid duplicating them.
@@ -1275,12 +1296,7 @@ async def q10_empty_dustbin(ctx: click.Context, device_id: str) -> None:
12751296

12761297
@session.command()
12771298
@click.option("--device_id", required=True, help="Device ID")
1278-
@click.option(
1279-
"--mode",
1280-
required=True,
1281-
type=click.Choice(["vac_and_mop", "vacuum", "mop"], case_sensitive=False),
1282-
help='Clean mode (preferred: "vac_and_mop", "vacuum", "mop")',
1283-
)
1299+
@click.option("--mode", required=True, type=click.Choice(["bothwork", "onlysweep", "onlymop"]), help="Clean mode")
12841300
@click.pass_context
12851301
@async_command
12861302
async def q10_set_clean_mode(ctx: click.Context, device_id: str, mode: str) -> None:

roborock/const.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@
5353
ROBOROCK_QREVO_MAXV = "roborock.vacuum.a87"
5454
ROBOROCK_SAROS_10R = "roborock.vacuum.a144"
5555
ROBOROCK_SAROS_10 = "roborock.vacuum.a147"
56+
ROBOROCK_QREVO_CURV_2_FLOW = "roborock.vacuum.a245" # Qrevo Curv 2 Flow (CES 2026) — confirmed via live device probe
57+
58+
# Vivian series — a155/a156 unconfirmed CN/global Vivian variants.
59+
# NOTE: a245 (Qrevo Curv 2 Flow) was confirmed separately; it is NOT one of these.
60+
ROBOROCK_QREVO_CURV_A155 = "roborock.vacuum.a155" # Unconfirmed Vivian variant (CN)
61+
ROBOROCK_QREVO_CURV_A156 = "roborock.vacuum.a156" # Unconfirmed Vivian variant
62+
63+
# VivianC series (a158/a159) — single-line camera, spin-mop, max_plus clean mode.
64+
# Model names unconfirmed; update once device data is available.
65+
ROBOROCK_VIVIAN_C_A158 = "roborock.vacuum.a158"
66+
ROBOROCK_VIVIAN_C_A159 = "roborock.vacuum.a159"
5667

5768
ROBOROCK_DYAD_AIR = "roborock.wetdryvac.a107"
5869
ROBOROCK_DYAD_PRO_COMBO = "roborock.wetdryvac.a83"

0 commit comments

Comments
 (0)