python-roborock 5.20.1__py3-none-any.whl → 5.22.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_roborock-5.20.1.dist-info → python_roborock-5.22.0.dist-info}/METADATA +1 -1
- {python_roborock-5.20.1.dist-info → python_roborock-5.22.0.dist-info}/RECORD +21 -13
- roborock/cli.py +221 -5
- roborock/data/b01_q10/b01_q10_code_mappings.py +33 -3
- roborock/data/b01_q10/b01_q10_containers.py +118 -17
- roborock/data/code_mappings.py +17 -9
- roborock/devices/traits/b01/q10/__init__.py +59 -3
- roborock/devices/traits/b01/q10/button_light.py +29 -0
- roborock/devices/traits/b01/q10/child_lock.py +36 -0
- roborock/devices/traits/b01/q10/common.py +70 -0
- roborock/devices/traits/b01/q10/consumable.py +21 -0
- roborock/devices/traits/b01/q10/do_not_disturb.py +36 -0
- roborock/devices/traits/b01/q10/dust_collection.py +43 -0
- roborock/devices/traits/b01/q10/network_info.py +21 -0
- roborock/devices/traits/b01/q10/remote.py +1 -1
- roborock/devices/traits/b01/q10/status.py +9 -14
- roborock/devices/traits/b01/q10/vacuum.py +59 -31
- roborock/devices/traits/b01/q10/volume.py +29 -0
- {python_roborock-5.20.1.dist-info → python_roborock-5.22.0.dist-info}/WHEEL +0 -0
- {python_roborock-5.20.1.dist-info → python_roborock-5.22.0.dist-info}/entry_points.txt +0 -0
- {python_roborock-5.20.1.dist-info → python_roborock-5.22.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.22.0
|
|
4
4
|
Summary: A package to control Roborock vacuums.
|
|
5
5
|
Project-URL: Repository, https://github.com/python-roborock/python-roborock
|
|
6
6
|
Project-URL: Documentation, https://python-roborock.readthedocs.io/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
roborock/__init__.py,sha256=jpp3MEmevRvoJ5tq2waCDGH5VfJk--Lv1DmrwEdDyEQ,375
|
|
2
2
|
roborock/broadcast_protocol.py,sha256=fjNQW7NdBKRzSy6126TfCSoYGpT2Esj9Jd-t7tXFc_E,4015
|
|
3
3
|
roborock/callbacks.py,sha256=w2yPXtWFH7zfoFsLPYoCTbfwbE9p1ivA7iDcw20lJQI,4300
|
|
4
|
-
roborock/cli.py,sha256=
|
|
4
|
+
roborock/cli.py,sha256=3AZHW8Co1H0Bqi7o1nMKA2MkfPuld_OdbPHQh_sNzCA,63478
|
|
5
5
|
roborock/const.py,sha256=fWpgUPrCgLnaXmncgjCLZET-t7wHWQlpyzV8qHtTjb4,2860
|
|
6
6
|
roborock/device_features.py,sha256=ltWheUjsaOHm-Naw24evRkDUVBT9gB2l4wX-meI_dWo,35960
|
|
7
7
|
roborock/diagnostics.py,sha256=YIc7BhC6Cy6W2a6hykvfSRPG8NpLrPZCc_7jTX2Zdho,5091
|
|
@@ -13,11 +13,11 @@ roborock/roborock_typing.py,sha256=Rwx0c0oZCMY-4O8VMLnFSO_OUNb_D59cyG_ktZTZoZI,1
|
|
|
13
13
|
roborock/util.py,sha256=mjw329ujLjoWPPAr9mG0VZstaVL06dmVvNnYACB-Pxk,1650
|
|
14
14
|
roborock/web_api.py,sha256=M_A4PiRK5cwkoT3oS8fvglOU97s1jpWKuuCxeeCiF_E,37507
|
|
15
15
|
roborock/data/__init__.py,sha256=DikqaVM7VPVddkBNkHSss5zb1_iKB7GYmc2GiHoMAkA,257
|
|
16
|
-
roborock/data/code_mappings.py,sha256=
|
|
16
|
+
roborock/data/code_mappings.py,sha256=Cj1nEQkv3Vko7Mh8FODNtfIS5gn5KlTq2QXFtcincHo,8948
|
|
17
17
|
roborock/data/containers.py,sha256=-b6H2snGQTzsJSsctgil6sx8rLtCDihTTzdkxcFRdlE,16489
|
|
18
18
|
roborock/data/b01_q10/__init__.py,sha256=E4lwJcz4AI1z92FcYOJ-cY3fcfHSoXqvYVgfA-TgaRk,71
|
|
19
|
-
roborock/data/b01_q10/b01_q10_code_mappings.py,sha256=
|
|
20
|
-
roborock/data/b01_q10/b01_q10_containers.py,sha256=
|
|
19
|
+
roborock/data/b01_q10/b01_q10_code_mappings.py,sha256=NDKLgm9UvsRo90rLFvEkZ-eD5QbA_-hqgSSHa6DkhLM,9883
|
|
20
|
+
roborock/data/b01_q10/b01_q10_containers.py,sha256=YD08ZJnHv-ItK6guYtj-cH0bbYsvD2kMCfKat9DblBI,8628
|
|
21
21
|
roborock/data/b01_q7/__init__.py,sha256=D-Cd8eB0njf1PQPM6Exl_9A0AhpzIjrL0hapGOCEkkA,69
|
|
22
22
|
roborock/data/b01_q7/b01_q7_code_mappings.py,sha256=EHSCZSv_Os049NcenKo4tkPjDOB4vpmQ2p2Psr9FRCc,10698
|
|
23
23
|
roborock/data/b01_q7/b01_q7_containers.py,sha256=jNEHoNEdq276XJZGGUmtSyJ81cSIzJwem-O_NOgmfdQ,10270
|
|
@@ -47,12 +47,20 @@ roborock/devices/traits/common.py,sha256=KUlXvx2UcFVeNXEhPiCAdy2APu7zrJmM_jME_bM
|
|
|
47
47
|
roborock/devices/traits/traits_mixin.py,sha256=7t15l_Ty1cN-3-fKNtRH1XR6-mfadPnTBCQRZP4rAtc,2092
|
|
48
48
|
roborock/devices/traits/a01/__init__.py,sha256=i-us9zLPWPfSQ5XWq8t0MiYLYe5NkcLvk3aEQTYT7E0,8104
|
|
49
49
|
roborock/devices/traits/b01/__init__.py,sha256=pGN6wEocE6Fr2BTasG8Z_75crD6OdTwpju59Vw7eTDk,202
|
|
50
|
-
roborock/devices/traits/b01/q10/__init__.py,sha256=
|
|
50
|
+
roborock/devices/traits/b01/q10/__init__.py,sha256=CGzhddpgCvEJF-H9ZbZ1P89XNcPf2wcpBMn-1JVliN8,5803
|
|
51
|
+
roborock/devices/traits/b01/q10/button_light.py,sha256=_CCWSjFe_iZnnSXgBHOju31VUmwG1KKd_jGkmSpLew4,994
|
|
52
|
+
roborock/devices/traits/b01/q10/child_lock.py,sha256=bTsbAMxn7AF1zpWteyZCkQm4jj3NiWD8ddZTM3tlG_0,1131
|
|
51
53
|
roborock/devices/traits/b01/q10/command.py,sha256=mcWNRSq72bfa79_EjKRgMEIDTV3vWFh2ofBe4aF7EOE,1306
|
|
54
|
+
roborock/devices/traits/b01/q10/common.py,sha256=Qxl3zBAUulqdrjFZJ0DPEFEIJLvlPqUKuTQGPloy7Ds,3089
|
|
55
|
+
roborock/devices/traits/b01/q10/consumable.py,sha256=MbtMgYKqzNdVn6M1iyOAqZPCYTbN_Q_Pr4h8EYGEiuY,669
|
|
56
|
+
roborock/devices/traits/b01/q10/do_not_disturb.py,sha256=klyGQZ_nHyO6bhPlwBl8vElTnMwPf4A8VXwxkU8rMlM,1157
|
|
57
|
+
roborock/devices/traits/b01/q10/dust_collection.py,sha256=LM-0YH4pMrEqLUCFP2g-DymhL5ZUQ4bHd-2EyKHmucY,1582
|
|
52
58
|
roborock/devices/traits/b01/q10/map.py,sha256=ETaPWIH1qpiZQrutwm3aJu-WxS-idxLtjxntRduE70Q,4170
|
|
53
|
-
roborock/devices/traits/b01/q10/
|
|
54
|
-
roborock/devices/traits/b01/q10/
|
|
55
|
-
roborock/devices/traits/b01/q10/
|
|
59
|
+
roborock/devices/traits/b01/q10/network_info.py,sha256=XiCr6YPu7xkvhgeaGwXli-_4SB88QW63ykiegyaau2k,656
|
|
60
|
+
roborock/devices/traits/b01/q10/remote.py,sha256=8-fg4SA5M-tdMufHPeD9hIcax7y9Qh7pzcE7rBBVW_o,1267
|
|
61
|
+
roborock/devices/traits/b01/q10/status.py,sha256=JAPixbsQ8jfN3OvI7o_NxISXFk8l7rGHFYcL4pH97Jk,859
|
|
62
|
+
roborock/devices/traits/b01/q10/vacuum.py,sha256=w2FftNH5n-Tb-9drR3-5OiMy85FJfTDNmtt34LklC8w,4374
|
|
63
|
+
roborock/devices/traits/b01/q10/volume.py,sha256=UnT0yYDQntxYwnpv4_NVychm0FnGAYRAWA_FJR4yJag,1001
|
|
56
64
|
roborock/devices/traits/b01/q7/__init__.py,sha256=gHgIK-KBLDJDEneBmSJfTa38sumj1IuZl-lcvoZeYLY,8116
|
|
57
65
|
roborock/devices/traits/b01/q7/clean_summary.py,sha256=Tod29LUHBsOS0XL8SU1SdBQEHWTCifGZVCwSWGhYpy8,2933
|
|
58
66
|
roborock/devices/traits/b01/q7/map.py,sha256=Zrvj8LNctIFksCBoiZVQ1x4hgG3wwp1jqPY-If__JXw,1489
|
|
@@ -101,8 +109,8 @@ roborock/protocols/a01_protocol.py,sha256=JCIhUuVNamcENu0gtZR2x8rBz6dpAZLcbA-NQg
|
|
|
101
109
|
roborock/protocols/b01_q10_protocol.py,sha256=Ac3_VHJ2DAwBiXFMPYOHExEV0prxS9Ju0djiC8nKtiI,4935
|
|
102
110
|
roborock/protocols/b01_q7_protocol.py,sha256=o8repRrxhxknzspsg1K27ds643rs87ilWXbj8RJtOCs,4619
|
|
103
111
|
roborock/protocols/v1_protocol.py,sha256=uA-EMXd1AUBAXjkjAm0VxZlPLre0v2X9Kei5UqyPzaY,11141
|
|
104
|
-
python_roborock-5.
|
|
105
|
-
python_roborock-5.
|
|
106
|
-
python_roborock-5.
|
|
107
|
-
python_roborock-5.
|
|
108
|
-
python_roborock-5.
|
|
112
|
+
python_roborock-5.22.0.dist-info/METADATA,sha256=5-phrqS20MJVmIfW1RzPIpiWW2LhAJOra6wFGM9V7ho,5281
|
|
113
|
+
python_roborock-5.22.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
114
|
+
python_roborock-5.22.0.dist-info/entry_points.txt,sha256=EvC1nMqi9ZXKgZnqlNXA33v_3nzgPNjnM0mzWlrehnY,47
|
|
115
|
+
python_roborock-5.22.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
116
|
+
python_roborock-5.22.0.dist-info/RECORD,,
|
roborock/cli.py
CHANGED
|
@@ -49,7 +49,12 @@ except ImportError as err:
|
|
|
49
49
|
|
|
50
50
|
from roborock import RoborockCommand
|
|
51
51
|
from roborock.data import RoborockBase, UserData
|
|
52
|
-
from roborock.data.b01_q10.b01_q10_code_mappings import
|
|
52
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import (
|
|
53
|
+
B01_Q10_DP,
|
|
54
|
+
YXCleanType,
|
|
55
|
+
YXDeviceDustCollectionFrequency,
|
|
56
|
+
YXFanLevel,
|
|
57
|
+
)
|
|
53
58
|
from roborock.data.code_mappings import SHORT_MODEL_TO_ENUM
|
|
54
59
|
from roborock.device_features import DeviceFeatures
|
|
55
60
|
from roborock.devices.cache import Cache, CacheData
|
|
@@ -446,13 +451,67 @@ async def _display_v1_trait(context: RoborockContext, device_id: str, display_fu
|
|
|
446
451
|
click.echo(dump_json(trait.as_dict()))
|
|
447
452
|
|
|
448
453
|
|
|
449
|
-
async def
|
|
450
|
-
"""Get
|
|
454
|
+
async def _q10_properties(context: RoborockContext, device_id: str) -> Q10PropertiesApi:
|
|
455
|
+
"""Get the B01 Q10 properties API for a device."""
|
|
451
456
|
device_manager = await context.get_device_manager()
|
|
452
457
|
device = await device_manager.get_device(device_id)
|
|
453
458
|
if device.b01_q10_properties is None:
|
|
454
459
|
raise RoborockUnsupportedFeature("Device does not support B01 Q10 protocol. Is it a Q10?")
|
|
455
|
-
return device.b01_q10_properties
|
|
460
|
+
return device.b01_q10_properties
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
async def _q10_vacuum_trait(context: RoborockContext, device_id: str) -> VacuumTrait:
|
|
464
|
+
"""Get VacuumTrait from Q10 device."""
|
|
465
|
+
return (await _q10_properties(context, device_id)).vacuum
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
async def _display_q10_status(context: RoborockContext, device_id: str) -> None:
|
|
469
|
+
"""Refresh and display the full status of a B01 Q10 device.
|
|
470
|
+
|
|
471
|
+
Unlike V1 devices, the Q10 reports its state asynchronously: ``refresh()``
|
|
472
|
+
sends a request and the device streams the values back over the persistent
|
|
473
|
+
subscribe loop. That loop also delivers unsolicited pushes, so the read-model
|
|
474
|
+
traits may already hold (possibly stale) values from before this command ran
|
|
475
|
+
-- checking that a field is merely populated isn't enough. To display data
|
|
476
|
+
the device sent *in response to this refresh*, we register update listeners,
|
|
477
|
+
fire the refresh, and wait for a fresh update before reading the traits.
|
|
478
|
+
|
|
479
|
+
All read-model traits refreshed by :meth:`Q10PropertiesApi.refresh` are shown,
|
|
480
|
+
not just ``status`` (volume, child lock, do-not-disturb, dust collection,
|
|
481
|
+
network info and consumables are part of the device's reported state too).
|
|
482
|
+
"""
|
|
483
|
+
properties = await _q10_properties(context, device_id)
|
|
484
|
+
|
|
485
|
+
# Read-model traits populated from the device's DPS push stream.
|
|
486
|
+
traits = {
|
|
487
|
+
"status": properties.status,
|
|
488
|
+
"volume": properties.volume,
|
|
489
|
+
"child_lock": properties.child_lock,
|
|
490
|
+
"do_not_disturb": properties.do_not_disturb,
|
|
491
|
+
"dust_collection": properties.dust_collection,
|
|
492
|
+
"network_info": properties.network_info,
|
|
493
|
+
"consumable": properties.consumable,
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
updated = asyncio.Event()
|
|
497
|
+
unsubscribes = [trait.add_update_listener(updated.set) for trait in traits.values()]
|
|
498
|
+
try:
|
|
499
|
+
await properties.refresh()
|
|
500
|
+
try:
|
|
501
|
+
await asyncio.wait_for(updated.wait(), timeout=5)
|
|
502
|
+
except TimeoutError:
|
|
503
|
+
click.echo("Timed out waiting for status from device")
|
|
504
|
+
return
|
|
505
|
+
# The device streams its DPS across several pushes; give the remaining
|
|
506
|
+
# ones a brief window to arrive after the first fresh update.
|
|
507
|
+
await asyncio.sleep(0.5)
|
|
508
|
+
finally:
|
|
509
|
+
for unsubscribe in unsubscribes:
|
|
510
|
+
unsubscribe()
|
|
511
|
+
|
|
512
|
+
# Each concrete trait also subclasses a RoborockBase read-model, so it has
|
|
513
|
+
# ``as_dict``; the cast satisfies the typed UpdatableTrait view above.
|
|
514
|
+
click.echo(dump_json({name: cast(RoborockBase, trait).as_dict() for name, trait in traits.items()}))
|
|
456
515
|
|
|
457
516
|
|
|
458
517
|
@session.command()
|
|
@@ -462,7 +521,14 @@ async def _q10_vacuum_trait(context: RoborockContext, device_id: str) -> VacuumT
|
|
|
462
521
|
async def status(ctx, device_id: str):
|
|
463
522
|
"""Get device status."""
|
|
464
523
|
context: RoborockContext = ctx.obj
|
|
465
|
-
await
|
|
524
|
+
device_manager = await context.get_device_manager()
|
|
525
|
+
device = await device_manager.get_device(device_id)
|
|
526
|
+
if device.v1_properties is not None:
|
|
527
|
+
await _display_v1_trait(context, device_id, lambda v1: v1.status)
|
|
528
|
+
elif device.b01_q10_properties is not None:
|
|
529
|
+
await _display_q10_status(context, device_id)
|
|
530
|
+
else:
|
|
531
|
+
click.echo("Feature not supported by device")
|
|
466
532
|
|
|
467
533
|
|
|
468
534
|
@session.command()
|
|
@@ -1399,6 +1465,156 @@ async def q10_vacuum_dock(ctx: click.Context, device_id: str) -> None:
|
|
|
1399
1465
|
click.echo(f"Error: {e}")
|
|
1400
1466
|
|
|
1401
1467
|
|
|
1468
|
+
@session.command()
|
|
1469
|
+
@click.option("--device_id", required=True, help="Device ID")
|
|
1470
|
+
@click.pass_context
|
|
1471
|
+
@async_command
|
|
1472
|
+
async def q10_vacuum_spot(ctx: click.Context, device_id: str) -> None:
|
|
1473
|
+
"""Start a spot / part clean on a Q10 device."""
|
|
1474
|
+
context: RoborockContext = ctx.obj
|
|
1475
|
+
try:
|
|
1476
|
+
trait = await _q10_vacuum_trait(context, device_id)
|
|
1477
|
+
await trait.spot_clean()
|
|
1478
|
+
click.echo("Starting spot clean...")
|
|
1479
|
+
except RoborockUnsupportedFeature:
|
|
1480
|
+
click.echo("Device does not support B01 Q10 protocol. Is it a Q10?")
|
|
1481
|
+
except RoborockException as e:
|
|
1482
|
+
click.echo(f"Error: {e}")
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
@session.command()
|
|
1486
|
+
@click.option("--device_id", required=True, help="Device ID")
|
|
1487
|
+
@click.option(
|
|
1488
|
+
"--segments",
|
|
1489
|
+
required=True,
|
|
1490
|
+
help="Comma-separated room/segment ids to clean (see the `rooms` command), e.g. 9,2",
|
|
1491
|
+
)
|
|
1492
|
+
@click.pass_context
|
|
1493
|
+
@async_command
|
|
1494
|
+
async def q10_clean_segments(ctx: click.Context, device_id: str, segments: str) -> None:
|
|
1495
|
+
"""Start a room / segment clean on a Q10 device.
|
|
1496
|
+
|
|
1497
|
+
Room ids come from the `rooms` command (the device's map rooms).
|
|
1498
|
+
"""
|
|
1499
|
+
context: RoborockContext = ctx.obj
|
|
1500
|
+
try:
|
|
1501
|
+
segment_ids = [int(s) for s in segments.split(",") if s.strip()]
|
|
1502
|
+
except ValueError:
|
|
1503
|
+
click.echo("--segments must be comma-separated integers, e.g. 9,2")
|
|
1504
|
+
return
|
|
1505
|
+
if not segment_ids:
|
|
1506
|
+
click.echo("No segment ids provided")
|
|
1507
|
+
return
|
|
1508
|
+
try:
|
|
1509
|
+
trait = await _q10_vacuum_trait(context, device_id)
|
|
1510
|
+
await trait.clean_segments(segment_ids)
|
|
1511
|
+
click.echo(f"Starting room clean of segments {segment_ids}...")
|
|
1512
|
+
except RoborockUnsupportedFeature:
|
|
1513
|
+
click.echo("Device does not support B01 Q10 protocol. Is it a Q10?")
|
|
1514
|
+
except RoborockException as e:
|
|
1515
|
+
click.echo(f"Error: {e}")
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
async def _q10_set(ctx: click.Context, device_id: str, apply: Callable[[Any], Any], message: str) -> None:
|
|
1519
|
+
"""Run a Q10 settings write and report the result."""
|
|
1520
|
+
context: RoborockContext = ctx.obj
|
|
1521
|
+
try:
|
|
1522
|
+
properties = await _q10_properties(context, device_id)
|
|
1523
|
+
await apply(properties)
|
|
1524
|
+
click.echo(message)
|
|
1525
|
+
except RoborockUnsupportedFeature:
|
|
1526
|
+
click.echo("Device does not support B01 Q10 protocol. Is it a Q10?")
|
|
1527
|
+
except (RoborockException, ValueError) as e:
|
|
1528
|
+
click.echo(f"Error: {e}")
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
@session.command()
|
|
1532
|
+
@click.option("--device_id", required=True, help="Device ID")
|
|
1533
|
+
@click.option("--volume", required=True, type=int, help="Volume 0-100")
|
|
1534
|
+
@click.pass_context
|
|
1535
|
+
@async_command
|
|
1536
|
+
async def q10_set_volume(ctx: click.Context, device_id: str, volume: int) -> None:
|
|
1537
|
+
"""Set the speaker volume on a Q10 device."""
|
|
1538
|
+
await _q10_set(ctx, device_id, lambda p: p.volume.set_volume(volume), f"Volume set to {volume}")
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
@session.command()
|
|
1542
|
+
@click.option("--device_id", required=True, help="Device ID")
|
|
1543
|
+
@click.option("--enabled", required=True, type=bool, help="Enable (True) or disable (False)")
|
|
1544
|
+
@click.pass_context
|
|
1545
|
+
@async_command
|
|
1546
|
+
async def q10_set_child_lock(ctx: click.Context, device_id: str, enabled: bool) -> None:
|
|
1547
|
+
"""Enable or disable the child lock on a Q10 device."""
|
|
1548
|
+
await _q10_set(
|
|
1549
|
+
ctx,
|
|
1550
|
+
device_id,
|
|
1551
|
+
lambda p: p.child_lock.enable() if enabled else p.child_lock.disable(),
|
|
1552
|
+
f"Child lock set to {enabled}",
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
@session.command()
|
|
1557
|
+
@click.option("--device_id", required=True, help="Device ID")
|
|
1558
|
+
@click.option("--enabled", required=True, type=bool, help="Enable (True) or disable (False)")
|
|
1559
|
+
@click.pass_context
|
|
1560
|
+
@async_command
|
|
1561
|
+
async def q10_set_dnd(ctx: click.Context, device_id: str, enabled: bool) -> None:
|
|
1562
|
+
"""Enable or disable Do Not Disturb on a Q10 device."""
|
|
1563
|
+
await _q10_set(
|
|
1564
|
+
ctx,
|
|
1565
|
+
device_id,
|
|
1566
|
+
lambda p: p.do_not_disturb.enable() if enabled else p.do_not_disturb.disable(),
|
|
1567
|
+
f"Do Not Disturb set to {enabled}",
|
|
1568
|
+
)
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
@session.command()
|
|
1572
|
+
@click.option("--device_id", required=True, help="Device ID")
|
|
1573
|
+
@click.option("--enabled", required=True, type=bool, help="Enable (True) or disable (False)")
|
|
1574
|
+
@click.pass_context
|
|
1575
|
+
@async_command
|
|
1576
|
+
async def q10_set_led(ctx: click.Context, device_id: str, enabled: bool) -> None:
|
|
1577
|
+
"""Enable or disable the indicator light (LED) on a Q10 device."""
|
|
1578
|
+
await _q10_set(
|
|
1579
|
+
ctx,
|
|
1580
|
+
device_id,
|
|
1581
|
+
lambda p: p.button_light.enable() if enabled else p.button_light.disable(),
|
|
1582
|
+
f"LED set to {enabled}",
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
@session.command()
|
|
1587
|
+
@click.option("--device_id", required=True, help="Device ID")
|
|
1588
|
+
@click.option("--enabled", required=True, type=bool, help="Enable (True) or disable (False)")
|
|
1589
|
+
@click.pass_context
|
|
1590
|
+
@async_command
|
|
1591
|
+
async def q10_set_dust_collection(ctx: click.Context, device_id: str, enabled: bool) -> None:
|
|
1592
|
+
"""Enable or disable automatic dust collection on a Q10 device."""
|
|
1593
|
+
await _q10_set(
|
|
1594
|
+
ctx,
|
|
1595
|
+
device_id,
|
|
1596
|
+
lambda p: p.dust_collection.enable() if enabled else p.dust_collection.disable(),
|
|
1597
|
+
f"Dust collection set to {enabled}",
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
@session.command()
|
|
1602
|
+
@click.option("--device_id", required=True, help="Device ID")
|
|
1603
|
+
@click.option(
|
|
1604
|
+
"--frequency",
|
|
1605
|
+
required=True,
|
|
1606
|
+
type=click.Choice([str(m.code) for m in YXDeviceDustCollectionFrequency]),
|
|
1607
|
+
help="Empty after every N cleans (0 = daily).",
|
|
1608
|
+
)
|
|
1609
|
+
@click.pass_context
|
|
1610
|
+
@async_command
|
|
1611
|
+
async def q10_set_dust_frequency(ctx: click.Context, device_id: str, frequency: str) -> None:
|
|
1612
|
+
"""Set how often the dock empties the bin (0 = daily, else every N cleans)."""
|
|
1613
|
+
freq = YXDeviceDustCollectionFrequency.from_code(int(frequency))
|
|
1614
|
+
label = "daily" if freq.code == 0 else f"every {freq.code} cleans"
|
|
1615
|
+
await _q10_set(ctx, device_id, lambda p: p.dust_collection.set_frequency(freq), f"Dust frequency set to {label}")
|
|
1616
|
+
|
|
1617
|
+
|
|
1402
1618
|
@session.command()
|
|
1403
1619
|
@click.option("--device_id", required=True, help="Device ID")
|
|
1404
1620
|
@click.pass_context
|
|
@@ -85,9 +85,14 @@ class B01_Q10_DP(RoborockModeEnum):
|
|
|
85
85
|
SUSPECTED_THRESHOLD = ("dpSuspectedThreshold", 99)
|
|
86
86
|
SUSPECTED_THRESHOLD_UP = ("dpSuspectedThresholdUp", 100)
|
|
87
87
|
COMMON = ("dpCommon", 101)
|
|
88
|
-
JUMP_SCAN = ("dpJumpScan", 101)
|
|
89
88
|
REQUEST_DPS = ("dpRequestDps", 102) # NOTE: typo "dpRequetdps" in source code
|
|
90
|
-
|
|
89
|
+
# NOTE: the legacy B01 source also listed dpJumpScan (101) and
|
|
90
|
+
# dpCliffRestrictedArea (102), which collided with the confirmed dpCommon /
|
|
91
|
+
# dpRequestDps codes above (verified against ss07 hardware and the official app
|
|
92
|
+
# plugin) and shadowed them in ``from_code``. Both are unused and their real
|
|
93
|
+
# codes could not be verified, so they were removed rather than left as wrong
|
|
94
|
+
# duplicates. dpCliffRestrictedAreaUp (103) is kept: ss07 hardware does push
|
|
95
|
+
# data point 103 (an empty list when no cliff-restricted areas are set).
|
|
91
96
|
CLIFF_RESTRICTED_AREA_UP = ("dpCliffRestrictedAreaUp", 103)
|
|
92
97
|
BREAKPOINT_CLEAN = ("dpBreakpointClean", 104)
|
|
93
98
|
VALLEY_POINT_CHARGING = ("dpValleyPointCharging", 105)
|
|
@@ -96,6 +101,13 @@ class B01_Q10_DP(RoborockModeEnum):
|
|
|
96
101
|
VOICE_VERSION = ("dpVoiceVersion", 108)
|
|
97
102
|
ROBOT_COUNTRY_CODE = ("dpRobotCountryCode", 109)
|
|
98
103
|
HEARTBEAT = ("dpHeartbeat", 110)
|
|
104
|
+
# NOTE: ss07 hardware also pushes data points 112 and 113 in its full status
|
|
105
|
+
# dump. They are absent from the official app's vacuum plugin and stayed 0
|
|
106
|
+
# across every observed state (docked/charging, segment cleaning, lifted-off-
|
|
107
|
+
# ground fault, returning to dock, dustbin removed), so their meaning is not
|
|
108
|
+
# yet known. They are intentionally left unmapped; ``decode_rpc_response``
|
|
109
|
+
# silently ignores unknown codes via ``from_code_optional``, so they do not
|
|
110
|
+
# produce "not a valid code" warnings. Map them here once identified.
|
|
99
111
|
STATUS = ("dpStatus", 121)
|
|
100
112
|
BATTERY = ("dpBattery", 122)
|
|
101
113
|
FAN_LEVEL = ("dpFanLevel", 123) # NOTE: typo "dpfunLevel" in source code
|
|
@@ -215,13 +227,31 @@ class YXDeviceCleanTask(RoborockModeEnum):
|
|
|
215
227
|
|
|
216
228
|
|
|
217
229
|
class YXDeviceDustCollectionFrequency(RoborockModeEnum):
|
|
218
|
-
|
|
230
|
+
# The app exposes "regular" (code 0) vs "frequent", where "frequent" selects
|
|
231
|
+
# one of the every-N-cleans intervals below.
|
|
232
|
+
REGULAR = "regular", 0
|
|
219
233
|
INTERVAL_15 = "interval_15", 15
|
|
220
234
|
INTERVAL_30 = "interval_30", 30
|
|
221
235
|
INTERVAL_45 = "interval_45", 45
|
|
222
236
|
INTERVAL_60 = "interval_60", 60
|
|
223
237
|
|
|
224
238
|
|
|
239
|
+
class YXAreaUnit(RoborockModeEnum):
|
|
240
|
+
"""Unit used to report cleaned area (dpAreaUnit)."""
|
|
241
|
+
|
|
242
|
+
SQUARE_METER = "square_meter", 0
|
|
243
|
+
SQUARE_FEET = "square_feet", 1
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class YXCarpetCleanType(RoborockModeEnum):
|
|
247
|
+
"""Carpet handling behavior (dpCarpetCleanType)."""
|
|
248
|
+
|
|
249
|
+
RISE = "rise", 0 # lift the mop and boost over carpet
|
|
250
|
+
AVOID = "avoid", 1
|
|
251
|
+
IGNORE = "ignore", 2
|
|
252
|
+
CROSS = "cross", 3
|
|
253
|
+
|
|
254
|
+
|
|
225
255
|
class RemoteCommand(IntEnum):
|
|
226
256
|
FORWARD = 0
|
|
227
257
|
LEFT = 2
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Many of these classes use the `field(metadata={"dps": ...})` convention to map
|
|
4
4
|
dataclass fields to device Data Points (DPS). This metadata is utilized by the
|
|
5
|
-
`
|
|
5
|
+
`UpdatableTrait` helper in `roborock.devices.traits.b01.q10.common` to
|
|
6
6
|
automatically update objects from raw device responses.
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -11,9 +11,13 @@ from dataclasses import dataclass, field
|
|
|
11
11
|
from ..containers import RoborockBase
|
|
12
12
|
from .b01_q10_code_mappings import (
|
|
13
13
|
B01_Q10_DP,
|
|
14
|
+
YXAreaUnit,
|
|
14
15
|
YXBackType,
|
|
16
|
+
YXCarpetCleanType,
|
|
17
|
+
YXCleanLine,
|
|
15
18
|
YXCleanType,
|
|
16
19
|
YXDeviceCleanTask,
|
|
20
|
+
YXDeviceDustCollectionFrequency,
|
|
17
21
|
YXDeviceState,
|
|
18
22
|
YXFanLevel,
|
|
19
23
|
YXWaterLevel,
|
|
@@ -51,18 +55,24 @@ class dpSelfIdentifyingCarpet(RoborockBase):
|
|
|
51
55
|
|
|
52
56
|
@dataclass
|
|
53
57
|
class dpNetInfo(RoborockBase):
|
|
54
|
-
|
|
55
|
-
ipAdress
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
wifi_name: str | None = None
|
|
59
|
+
# "ip_adress" intentionally mirrors the device's "ipAdress" key (sic).
|
|
60
|
+
ip_adress: str | None = None
|
|
61
|
+
mac: str | None = None
|
|
62
|
+
signal: int | None = None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def ip_address(self) -> str | None:
|
|
66
|
+
"""Correctly-spelled alias for :attr:`ip_adress`."""
|
|
67
|
+
return self.ip_adress
|
|
58
68
|
|
|
59
69
|
|
|
60
70
|
@dataclass
|
|
61
71
|
class dpNotDisturbExpand(RoborockBase):
|
|
62
|
-
disturb_dust_enable: int
|
|
63
|
-
disturb_light: int
|
|
64
|
-
disturb_resume_clean: int
|
|
65
|
-
disturb_voice: int
|
|
72
|
+
disturb_dust_enable: int | None = None
|
|
73
|
+
disturb_light: int | None = None
|
|
74
|
+
disturb_resume_clean: int | None = None
|
|
75
|
+
disturb_voice: int | None = None
|
|
66
76
|
|
|
67
77
|
|
|
68
78
|
@dataclass
|
|
@@ -77,16 +87,18 @@ class dpVoiceVersion(RoborockBase):
|
|
|
77
87
|
|
|
78
88
|
@dataclass
|
|
79
89
|
class dpTimeZone(RoborockBase):
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
time_zone_city: str | None = None
|
|
91
|
+
time_zone_sec: int | None = None
|
|
82
92
|
|
|
83
93
|
|
|
84
94
|
@dataclass
|
|
85
95
|
class Q10Status(RoborockBase):
|
|
86
|
-
"""
|
|
96
|
+
"""Core vacuum status for Q10 devices.
|
|
87
97
|
|
|
88
98
|
Fields are mapped to DPS values using metadata. Objects of this class can be
|
|
89
|
-
automatically updated using the `
|
|
99
|
+
automatically updated using the `UpdatableTrait` helper. Settings that have
|
|
100
|
+
their own trait (volume, child lock, do-not-disturb, dust collection,
|
|
101
|
+
network info, consumables) live on those traits instead of here.
|
|
90
102
|
"""
|
|
91
103
|
|
|
92
104
|
clean_time: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TIME})
|
|
@@ -99,12 +111,101 @@ class Q10Status(RoborockBase):
|
|
|
99
111
|
total_clean_area: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_AREA})
|
|
100
112
|
total_clean_count: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_COUNT})
|
|
101
113
|
total_clean_time: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_TIME})
|
|
102
|
-
main_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.MAIN_BRUSH_LIFE})
|
|
103
|
-
side_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SIDE_BRUSH_LIFE})
|
|
104
|
-
filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
|
|
105
|
-
sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
|
|
106
114
|
clean_mode: YXCleanType | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_MODE})
|
|
107
115
|
clean_task_type: YXDeviceCleanTask | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TASK_TYPE})
|
|
108
116
|
back_type: YXBackType | None = field(default=None, metadata={"dps": B01_Q10_DP.BACK_TYPE})
|
|
109
117
|
cleaning_progress: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_PROGRESS})
|
|
110
118
|
fault: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FAULT})
|
|
119
|
+
|
|
120
|
+
# Additional state reported in the device's full status dump.
|
|
121
|
+
clean_line: YXCleanLine | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_LINE})
|
|
122
|
+
carpet_clean_type: YXCarpetCleanType | None = field(default=None, metadata={"dps": B01_Q10_DP.CARPET_CLEAN_TYPE})
|
|
123
|
+
area_unit: YXAreaUnit | None = field(default=None, metadata={"dps": B01_Q10_DP.AREA_UNIT})
|
|
124
|
+
auto_boost: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.AUTO_BOOST})
|
|
125
|
+
multi_map_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MULTI_MAP_SWITCH})
|
|
126
|
+
map_save_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MAP_SAVE_SWITCH})
|
|
127
|
+
recent_clean_record: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.RECENT_CLEAN_RECORD})
|
|
128
|
+
valley_point_charging: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.VALLEY_POINT_CHARGING})
|
|
129
|
+
line_laser_obstacle_avoidance: bool | None = field(
|
|
130
|
+
default=None, metadata={"dps": B01_Q10_DP.LINE_LASER_OBSTACLE_AVOIDANCE}
|
|
131
|
+
)
|
|
132
|
+
# Whether a mop module is attached, and whether "clean along floor direction" is on.
|
|
133
|
+
mop_state: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MOP_STATE})
|
|
134
|
+
ground_clean: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.GROUND_CLEAN})
|
|
135
|
+
# True while an "add area" / re-clean (the app's draw-a-rectangle "re cleaning")
|
|
136
|
+
# request is in progress; pulses back to False once the robot has the area.
|
|
137
|
+
add_clean_state: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.ADD_CLEAN_STATE})
|
|
138
|
+
robot_country_code: str | None = field(default=None, metadata={"dps": B01_Q10_DP.ROBOT_COUNTRY_CODE})
|
|
139
|
+
time_zone: dpTimeZone | None = field(default=None, metadata={"dps": B01_Q10_DP.TIME_ZONE})
|
|
140
|
+
|
|
141
|
+
# TODO(#846): value mappings for these ints are not yet decoded (no app
|
|
142
|
+
# control found / internal / constant); keep as int until reverse-engineered.
|
|
143
|
+
breakpoint_clean: int | None = field(default=None, metadata={"dps": B01_Q10_DP.BREAKPOINT_CLEAN})
|
|
144
|
+
timer_type: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TIMER_TYPE})
|
|
145
|
+
user_plan: int | None = field(default=None, metadata={"dps": B01_Q10_DP.USER_PLAN})
|
|
146
|
+
robot_type: int | None = field(default=None, metadata={"dps": B01_Q10_DP.ROBOT_TYPE})
|
|
147
|
+
|
|
148
|
+
# DEPRECATED: consumable/accessory remaining-life now lives on the
|
|
149
|
+
# ``Q10Consumable`` trait. These aliases are kept here for backwards
|
|
150
|
+
# compatibility and will be removed in a follow-up release. See PR #846.
|
|
151
|
+
main_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.MAIN_BRUSH_LIFE})
|
|
152
|
+
side_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SIDE_BRUSH_LIFE})
|
|
153
|
+
filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
|
|
154
|
+
sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class SoundVolume(RoborockBase):
|
|
159
|
+
"""Speaker volume read-model (0-100)."""
|
|
160
|
+
|
|
161
|
+
volume: int | None = field(default=None, metadata={"dps": B01_Q10_DP.VOLUME})
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class ChildLock(RoborockBase):
|
|
166
|
+
"""Child-lock read-model."""
|
|
167
|
+
|
|
168
|
+
child_lock: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.CHILD_LOCK})
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class DoNotDisturb(RoborockBase):
|
|
173
|
+
"""Do Not Disturb read-model."""
|
|
174
|
+
|
|
175
|
+
not_disturb: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.NOT_DISTURB})
|
|
176
|
+
not_disturb_expand: dpNotDisturbExpand | None = field(default=None, metadata={"dps": B01_Q10_DP.NOT_DISTURB_EXPAND})
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class DustCollection(RoborockBase):
|
|
181
|
+
"""Dock auto-empty (dust collection) read-model."""
|
|
182
|
+
|
|
183
|
+
dust_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.DUST_SWITCH})
|
|
184
|
+
dust_setting: YXDeviceDustCollectionFrequency | None = field(
|
|
185
|
+
default=None, metadata={"dps": B01_Q10_DP.DUST_SETTING}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass
|
|
190
|
+
class Q10Consumable(RoborockBase):
|
|
191
|
+
"""Consumable / accessory remaining-life read-model.
|
|
192
|
+
|
|
193
|
+
Named with a ``Q10`` prefix to avoid shadowing the v1 ``Consumable`` when both
|
|
194
|
+
are star-imported into the ``roborock.data`` namespace.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
main_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.MAIN_BRUSH_LIFE})
|
|
198
|
+
side_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SIDE_BRUSH_LIFE})
|
|
199
|
+
filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
|
|
200
|
+
sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass
|
|
204
|
+
class Q10NetworkInfo(RoborockBase):
|
|
205
|
+
"""Network information read-model.
|
|
206
|
+
|
|
207
|
+
Named with a ``Q10`` prefix to avoid shadowing the v1 ``NetworkInfo`` when both
|
|
208
|
+
are star-imported into the ``roborock.data`` namespace.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
net_info: dpNetInfo | None = field(default=None, metadata={"dps": B01_Q10_DP.NET_INFO})
|
roborock/data/code_mappings.py
CHANGED
|
@@ -76,11 +76,17 @@ class RoborockModeEnum(StrEnum):
|
|
|
76
76
|
|
|
77
77
|
@classmethod
|
|
78
78
|
def from_code_optional(cls, code: int) -> Self | None:
|
|
79
|
-
"""Gracefully return None if the code does not exist.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
"""Gracefully return None if the code does not exist.
|
|
80
|
+
|
|
81
|
+
This is the silent counterpart to :meth:`from_code`: callers use it when
|
|
82
|
+
an unknown code is expected and tolerable (e.g. decoding a device push
|
|
83
|
+
that may include data points this library does not model yet), so it must
|
|
84
|
+
not emit the "not a valid code" warning that ``from_code`` logs.
|
|
85
|
+
"""
|
|
86
|
+
for member in cls:
|
|
87
|
+
if member.code == code:
|
|
88
|
+
return member
|
|
89
|
+
return None
|
|
84
90
|
|
|
85
91
|
@classmethod
|
|
86
92
|
def from_value(cls, value: str) -> Self:
|
|
@@ -115,12 +121,14 @@ class RoborockModeEnum(StrEnum):
|
|
|
115
121
|
return cls.from_value(str(value))
|
|
116
122
|
except ValueError:
|
|
117
123
|
pass
|
|
118
|
-
# Try integer code lookup (e.g. "11")
|
|
124
|
+
# Try integer code lookup (e.g. "11"). Use the silent optional variant so
|
|
125
|
+
# a value that is neither a name, a DP string, nor a known code resolves
|
|
126
|
+
# to None without logging a spurious "not a valid code" warning.
|
|
119
127
|
try:
|
|
120
|
-
|
|
128
|
+
int_code = int(value)
|
|
121
129
|
except (ValueError, TypeError):
|
|
122
|
-
|
|
123
|
-
return
|
|
130
|
+
return None
|
|
131
|
+
return cls.from_code_optional(int_code)
|
|
124
132
|
|
|
125
133
|
@classmethod
|
|
126
134
|
def keys(cls) -> list[str]:
|
|
@@ -10,15 +10,30 @@ from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
|
10
10
|
from roborock.map.b01_q10_map_parser import Q10MapPacket, Q10TracePacket
|
|
11
11
|
from roborock.protocols.b01_q10_protocol import Q10DpsUpdate, Q10Message
|
|
12
12
|
|
|
13
|
+
from .button_light import ButtonLightTrait
|
|
14
|
+
from .child_lock import ChildLockTrait
|
|
13
15
|
from .command import CommandTrait
|
|
16
|
+
from .consumable import ConsumableTrait
|
|
17
|
+
from .do_not_disturb import DoNotDisturbTrait
|
|
18
|
+
from .dust_collection import DustCollectionTrait
|
|
14
19
|
from .map import MapContentTrait
|
|
20
|
+
from .network_info import NetworkInfoTrait
|
|
15
21
|
from .remote import RemoteTrait
|
|
16
22
|
from .status import StatusTrait
|
|
17
23
|
from .vacuum import VacuumTrait
|
|
24
|
+
from .volume import SoundVolumeTrait
|
|
18
25
|
|
|
19
26
|
__all__ = [
|
|
20
27
|
"Q10PropertiesApi",
|
|
28
|
+
"ButtonLightTrait",
|
|
29
|
+
"ChildLockTrait",
|
|
30
|
+
"ConsumableTrait",
|
|
31
|
+
"DoNotDisturbTrait",
|
|
32
|
+
"DustCollectionTrait",
|
|
21
33
|
"MapContentTrait",
|
|
34
|
+
"NetworkInfoTrait",
|
|
35
|
+
"SoundVolumeTrait",
|
|
36
|
+
"StatusTrait",
|
|
22
37
|
]
|
|
23
38
|
|
|
24
39
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -31,7 +46,7 @@ class Q10PropertiesApi(Trait):
|
|
|
31
46
|
"""Trait for sending commands to Q10 devices."""
|
|
32
47
|
|
|
33
48
|
status: StatusTrait
|
|
34
|
-
"""Trait for managing the status of Q10 devices."""
|
|
49
|
+
"""Trait for managing the core status of Q10 devices."""
|
|
35
50
|
|
|
36
51
|
vacuum: VacuumTrait
|
|
37
52
|
"""Trait for sending vacuum related commands to Q10 devices."""
|
|
@@ -39,6 +54,27 @@ class Q10PropertiesApi(Trait):
|
|
|
39
54
|
remote: RemoteTrait
|
|
40
55
|
"""Trait for sending remote control related commands to Q10 devices."""
|
|
41
56
|
|
|
57
|
+
volume: SoundVolumeTrait
|
|
58
|
+
"""Trait for reading / setting the speaker volume."""
|
|
59
|
+
|
|
60
|
+
child_lock: ChildLockTrait
|
|
61
|
+
"""Trait for reading / controlling the child lock."""
|
|
62
|
+
|
|
63
|
+
do_not_disturb: DoNotDisturbTrait
|
|
64
|
+
"""Trait for reading / controlling Do Not Disturb."""
|
|
65
|
+
|
|
66
|
+
dust_collection: DustCollectionTrait
|
|
67
|
+
"""Trait for reading / controlling dock auto-empty (dust collection)."""
|
|
68
|
+
|
|
69
|
+
button_light: ButtonLightTrait
|
|
70
|
+
"""Trait for controlling the indicator / button light (LED)."""
|
|
71
|
+
|
|
72
|
+
network_info: NetworkInfoTrait
|
|
73
|
+
"""Trait exposing the device's network information."""
|
|
74
|
+
|
|
75
|
+
consumable: ConsumableTrait
|
|
76
|
+
"""Trait exposing remaining life of consumables."""
|
|
77
|
+
|
|
42
78
|
map: MapContentTrait
|
|
43
79
|
"""Trait for fetching the current parsed map (image + rooms)."""
|
|
44
80
|
|
|
@@ -49,7 +85,24 @@ class Q10PropertiesApi(Trait):
|
|
|
49
85
|
self.vacuum = VacuumTrait(self.command)
|
|
50
86
|
self.remote = RemoteTrait(self.command)
|
|
51
87
|
self.status = StatusTrait()
|
|
88
|
+
self.volume = SoundVolumeTrait(self.command)
|
|
89
|
+
self.child_lock = ChildLockTrait(self.command)
|
|
90
|
+
self.do_not_disturb = DoNotDisturbTrait(self.command)
|
|
91
|
+
self.dust_collection = DustCollectionTrait(self.command)
|
|
92
|
+
self.button_light = ButtonLightTrait(self.command)
|
|
93
|
+
self.network_info = NetworkInfoTrait()
|
|
94
|
+
self.consumable = ConsumableTrait()
|
|
52
95
|
self.map = MapContentTrait()
|
|
96
|
+
# Read-model traits updated from the device's DPS push stream.
|
|
97
|
+
self._updatable_traits = [
|
|
98
|
+
self.status,
|
|
99
|
+
self.volume,
|
|
100
|
+
self.child_lock,
|
|
101
|
+
self.do_not_disturb,
|
|
102
|
+
self.dust_collection,
|
|
103
|
+
self.network_info,
|
|
104
|
+
self.consumable,
|
|
105
|
+
]
|
|
53
106
|
self._subscribe_task: asyncio.Task[None] | None = None
|
|
54
107
|
|
|
55
108
|
async def start(self) -> None:
|
|
@@ -83,7 +136,7 @@ class Q10PropertiesApi(Trait):
|
|
|
83
136
|
Map and trace packets arrive as protocol-301 ``MAP_RESPONSE`` pushes (the
|
|
84
137
|
Q10 is entirely push-driven: there is no synchronous get-map request, a
|
|
85
138
|
``dpRequestDps`` just nudges the device to publish its current map). DPS
|
|
86
|
-
updates feed the
|
|
139
|
+
updates feed the read-model traits. More traits can be dispatched here below.
|
|
87
140
|
"""
|
|
88
141
|
if isinstance(message, Q10MapPacket):
|
|
89
142
|
self.map.update_from_map_packet(message)
|
|
@@ -91,7 +144,10 @@ class Q10PropertiesApi(Trait):
|
|
|
91
144
|
self.map.update_from_trace_packet(message)
|
|
92
145
|
elif isinstance(message, Q10DpsUpdate):
|
|
93
146
|
_LOGGER.debug("Received Q10 status update: %s", message.dps)
|
|
94
|
-
|
|
147
|
+
# Notify all read-model traits about the new message; each trait
|
|
148
|
+
# only updates the fields that it is responsible for.
|
|
149
|
+
for trait in self._updatable_traits:
|
|
150
|
+
trait.update_from_dps(message.dps)
|
|
95
151
|
|
|
96
152
|
|
|
97
153
|
def create(channel: MqttChannel) -> Q10PropertiesApi:
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Indicator / button light (LED) trait for Q10 B01 devices."""
|
|
2
|
+
|
|
3
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
4
|
+
|
|
5
|
+
from .command import CommandTrait
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ButtonLightTrait:
|
|
9
|
+
"""Trait for controlling the indicator / button light (LED) of a Q10 device.
|
|
10
|
+
|
|
11
|
+
The device does not report the button-light state in its status dump, so
|
|
12
|
+
this trait is write-only (no read-back).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, command: CommandTrait) -> None:
|
|
16
|
+
"""Initialize the button light trait."""
|
|
17
|
+
self._command = command
|
|
18
|
+
|
|
19
|
+
async def _write(self, value: int) -> None:
|
|
20
|
+
"""Write the button-light data point via the dpCommon (101) wrapper."""
|
|
21
|
+
await self._command.send(B01_Q10_DP.COMMON, {str(B01_Q10_DP.BUTTON_LIGHT_SWITCH.code): value})
|
|
22
|
+
|
|
23
|
+
async def enable(self) -> None:
|
|
24
|
+
"""Turn the indicator light on."""
|
|
25
|
+
await self._write(1)
|
|
26
|
+
|
|
27
|
+
async def disable(self) -> None:
|
|
28
|
+
"""Turn the indicator light off."""
|
|
29
|
+
await self._write(0)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Child lock trait for Q10 B01 devices."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
6
|
+
from roborock.data.b01_q10.b01_q10_containers import ChildLock
|
|
7
|
+
from roborock.devices.traits.common import DpsDataConverter
|
|
8
|
+
|
|
9
|
+
from .command import CommandTrait
|
|
10
|
+
from .common import UpdatableTrait
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChildLockTrait(ChildLock, UpdatableTrait):
|
|
16
|
+
"""Trait for reading and controlling the child lock of a Q10 device."""
|
|
17
|
+
|
|
18
|
+
_CONVERTER = DpsDataConverter.from_dataclass(ChildLock)
|
|
19
|
+
|
|
20
|
+
def __init__(self, command: CommandTrait) -> None:
|
|
21
|
+
"""Initialize the child lock trait."""
|
|
22
|
+
ChildLock.__init__(self)
|
|
23
|
+
UpdatableTrait.__init__(self, command, _LOGGER)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_on(self) -> bool:
|
|
27
|
+
"""Return whether the child lock is enabled."""
|
|
28
|
+
return bool(self.child_lock)
|
|
29
|
+
|
|
30
|
+
async def enable(self) -> None:
|
|
31
|
+
"""Enable the child lock."""
|
|
32
|
+
await self._write(B01_Q10_DP.CHILD_LOCK, 1)
|
|
33
|
+
|
|
34
|
+
async def disable(self) -> None:
|
|
35
|
+
"""Disable the child lock."""
|
|
36
|
+
await self._write(B01_Q10_DP.CHILD_LOCK, 0)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Common helpers for Q10 B01 traits.
|
|
2
|
+
|
|
3
|
+
Q10 devices push their full state as a single decoded DPS dictionary (see
|
|
4
|
+
``Q10PropertiesApi._subscribe_loop``). Each trait owns a small ``RoborockBase``
|
|
5
|
+
read-model whose fields are annotated with ``field(metadata={"dps": ...})`` and
|
|
6
|
+
only updates the fields it is responsible for, ignoring the rest.
|
|
7
|
+
|
|
8
|
+
The :class:`UpdatableTrait` base wires that read-model to the update lifecycle
|
|
9
|
+
and (for traits that also write) exposes the ``dpCommon`` (101) wrapper used by
|
|
10
|
+
Q10 setting writes.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, ClassVar, cast
|
|
15
|
+
|
|
16
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
17
|
+
from roborock.data.containers import RoborockBase
|
|
18
|
+
from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener
|
|
19
|
+
|
|
20
|
+
from .command import CommandTrait
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UpdatableTrait(TraitUpdateListener):
|
|
24
|
+
"""Base for Q10 traits backed by a read-model updated from the DPS stream.
|
|
25
|
+
|
|
26
|
+
Concrete traits subclass both their ``RoborockBase`` read-model and this
|
|
27
|
+
class, set the ``_CONVERTER`` class attribute, and initialize the read-model
|
|
28
|
+
explicitly in their constructor, e.g.::
|
|
29
|
+
|
|
30
|
+
class SoundVolumeTrait(SoundVolume, UpdatableTrait):
|
|
31
|
+
_CONVERTER = DpsDataConverter.from_dataclass(SoundVolume)
|
|
32
|
+
|
|
33
|
+
def __init__(self, command: CommandTrait) -> None:
|
|
34
|
+
SoundVolume.__init__(self)
|
|
35
|
+
UpdatableTrait.__init__(self, command, _LOGGER)
|
|
36
|
+
|
|
37
|
+
The read-model init is called explicitly (rather than via ``super()``)
|
|
38
|
+
because the read-model dataclass precedes this class in the MRO.
|
|
39
|
+
|
|
40
|
+
Traits that also send commands receive a :class:`CommandTrait` and use the
|
|
41
|
+
:meth:`_write` helper, which wraps the write in the ``dpCommon`` (101) data
|
|
42
|
+
point as the device requires.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
_CONVERTER: ClassVar[DpsDataConverter]
|
|
46
|
+
|
|
47
|
+
def __init__(self, command: CommandTrait | None, logger: logging.Logger) -> None:
|
|
48
|
+
"""Initialize the update listener. The read-model is initialized by the subclass."""
|
|
49
|
+
TraitUpdateListener.__init__(self, logger=logger)
|
|
50
|
+
self._command = command
|
|
51
|
+
|
|
52
|
+
def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None:
|
|
53
|
+
"""Update the trait's read-model from raw DPS data and notify listeners.
|
|
54
|
+
|
|
55
|
+
Concrete traits also subclass a ``RoborockBase`` read-model, so the cast
|
|
56
|
+
is always valid at runtime.
|
|
57
|
+
"""
|
|
58
|
+
if self._CONVERTER.update_from_dps(cast(RoborockBase, self), decoded_dps):
|
|
59
|
+
self._notify_update()
|
|
60
|
+
|
|
61
|
+
async def _write(self, dp: B01_Q10_DP, value: int) -> None:
|
|
62
|
+
"""Write a single data point value via the dpCommon (101) wrapper.
|
|
63
|
+
|
|
64
|
+
Q10 setting writes must be wrapped in ``dpCommon`` (101), e.g. setting
|
|
65
|
+
the volume sends ``{"dps": {"101": {"26": <value>}}}``. Writing the bare
|
|
66
|
+
data point (without the wrapper) is silently ignored by the device.
|
|
67
|
+
"""
|
|
68
|
+
if self._command is None:
|
|
69
|
+
raise ValueError("Trait is read-only; no command channel was provided")
|
|
70
|
+
await self._command.send(B01_Q10_DP.COMMON, {str(dp.code): value})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Consumable / accessory life trait for Q10 B01 devices."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from roborock.data.b01_q10.b01_q10_containers import Q10Consumable
|
|
6
|
+
from roborock.devices.traits.common import DpsDataConverter
|
|
7
|
+
|
|
8
|
+
from .common import UpdatableTrait
|
|
9
|
+
|
|
10
|
+
_LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConsumableTrait(Q10Consumable, UpdatableTrait):
|
|
14
|
+
"""Trait exposing remaining life of consumables (brushes, filter, sensors)."""
|
|
15
|
+
|
|
16
|
+
_CONVERTER = DpsDataConverter.from_dataclass(Q10Consumable)
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""Initialize the consumable trait."""
|
|
20
|
+
Q10Consumable.__init__(self)
|
|
21
|
+
UpdatableTrait.__init__(self, command=None, logger=_LOGGER)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Do Not Disturb trait for Q10 B01 devices."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
6
|
+
from roborock.data.b01_q10.b01_q10_containers import DoNotDisturb
|
|
7
|
+
from roborock.devices.traits.common import DpsDataConverter
|
|
8
|
+
|
|
9
|
+
from .command import CommandTrait
|
|
10
|
+
from .common import UpdatableTrait
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DoNotDisturbTrait(DoNotDisturb, UpdatableTrait):
|
|
16
|
+
"""Trait for reading and controlling Do Not Disturb on a Q10 device."""
|
|
17
|
+
|
|
18
|
+
_CONVERTER = DpsDataConverter.from_dataclass(DoNotDisturb)
|
|
19
|
+
|
|
20
|
+
def __init__(self, command: CommandTrait) -> None:
|
|
21
|
+
"""Initialize the Do Not Disturb trait."""
|
|
22
|
+
DoNotDisturb.__init__(self)
|
|
23
|
+
UpdatableTrait.__init__(self, command, _LOGGER)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_on(self) -> bool:
|
|
27
|
+
"""Return whether Do Not Disturb is enabled."""
|
|
28
|
+
return bool(self.not_disturb)
|
|
29
|
+
|
|
30
|
+
async def enable(self) -> None:
|
|
31
|
+
"""Enable Do Not Disturb."""
|
|
32
|
+
await self._write(B01_Q10_DP.NOT_DISTURB, 1)
|
|
33
|
+
|
|
34
|
+
async def disable(self) -> None:
|
|
35
|
+
"""Disable Do Not Disturb."""
|
|
36
|
+
await self._write(B01_Q10_DP.NOT_DISTURB, 0)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Dust collection (dock auto-empty) trait for Q10 B01 devices."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP, YXDeviceDustCollectionFrequency
|
|
6
|
+
from roborock.data.b01_q10.b01_q10_containers import DustCollection
|
|
7
|
+
from roborock.devices.traits.common import DpsDataConverter
|
|
8
|
+
|
|
9
|
+
from .command import CommandTrait
|
|
10
|
+
from .common import UpdatableTrait
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DustCollectionTrait(DustCollection, UpdatableTrait):
|
|
16
|
+
"""Trait for reading and controlling automatic dust collection at the dock."""
|
|
17
|
+
|
|
18
|
+
_CONVERTER = DpsDataConverter.from_dataclass(DustCollection)
|
|
19
|
+
|
|
20
|
+
def __init__(self, command: CommandTrait) -> None:
|
|
21
|
+
"""Initialize the dust collection trait."""
|
|
22
|
+
DustCollection.__init__(self)
|
|
23
|
+
UpdatableTrait.__init__(self, command, _LOGGER)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_on(self) -> bool:
|
|
27
|
+
"""Return whether automatic dust collection is enabled."""
|
|
28
|
+
return bool(self.dust_switch)
|
|
29
|
+
|
|
30
|
+
async def enable(self) -> None:
|
|
31
|
+
"""Enable automatic dust collection at the dock."""
|
|
32
|
+
await self._write(B01_Q10_DP.DUST_SWITCH, 1)
|
|
33
|
+
|
|
34
|
+
async def disable(self) -> None:
|
|
35
|
+
"""Disable automatic dust collection at the dock."""
|
|
36
|
+
await self._write(B01_Q10_DP.DUST_SWITCH, 0)
|
|
37
|
+
|
|
38
|
+
async def set_frequency(self, frequency: YXDeviceDustCollectionFrequency) -> None:
|
|
39
|
+
"""Set how often the dock empties the bin.
|
|
40
|
+
|
|
41
|
+
The value is the interval in cleans, with ``DAILY`` (0) meaning daily.
|
|
42
|
+
"""
|
|
43
|
+
await self._write(B01_Q10_DP.DUST_SETTING, frequency.code)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Network information trait for Q10 B01 devices."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from roborock.data.b01_q10.b01_q10_containers import Q10NetworkInfo
|
|
6
|
+
from roborock.devices.traits.common import DpsDataConverter
|
|
7
|
+
|
|
8
|
+
from .common import UpdatableTrait
|
|
9
|
+
|
|
10
|
+
_LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NetworkInfoTrait(Q10NetworkInfo, UpdatableTrait):
|
|
14
|
+
"""Trait exposing the device's network information (read-only)."""
|
|
15
|
+
|
|
16
|
+
_CONVERTER = DpsDataConverter.from_dataclass(Q10NetworkInfo)
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""Initialize the network info trait."""
|
|
20
|
+
Q10NetworkInfo.__init__(self)
|
|
21
|
+
UpdatableTrait.__init__(self, command=None, logger=_LOGGER)
|
|
@@ -20,7 +20,7 @@ class RemoteTrait:
|
|
|
20
20
|
self._command = command
|
|
21
21
|
|
|
22
22
|
async def _send_remote(self, action: RemoteCommand) -> None:
|
|
23
|
-
await self._command.send(B01_Q10_DP.COMMON, params={B01_Q10_DP.REMOTE: action.value})
|
|
23
|
+
await self._command.send(B01_Q10_DP.COMMON, params={str(B01_Q10_DP.REMOTE.code): action.value})
|
|
24
24
|
|
|
25
25
|
async def forward(self) -> None:
|
|
26
26
|
"""Move forward."""
|
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
"""Status trait for Q10 B01 devices."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Any
|
|
5
4
|
|
|
6
|
-
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
7
5
|
from roborock.data.b01_q10.b01_q10_containers import Q10Status
|
|
8
|
-
from roborock.devices.traits.common import DpsDataConverter
|
|
6
|
+
from roborock.devices.traits.common import DpsDataConverter
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
from .common import UpdatableTrait
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
11
|
|
|
14
12
|
|
|
15
|
-
class StatusTrait(Q10Status,
|
|
16
|
-
"""Trait for managing the status of Q10 Roborock devices.
|
|
13
|
+
class StatusTrait(Q10Status, UpdatableTrait):
|
|
14
|
+
"""Trait for managing the core status of Q10 Roborock devices.
|
|
17
15
|
|
|
18
16
|
This is a thin wrapper around Q10Status that provides the Trait interface.
|
|
19
17
|
The current values reflect the most recently received data from the device.
|
|
20
18
|
New values can be requested through the `Q10PropertiesApi`'s `refresh` method.
|
|
21
19
|
"""
|
|
22
20
|
|
|
21
|
+
_CONVERTER = DpsDataConverter.from_dataclass(Q10Status)
|
|
22
|
+
|
|
23
23
|
def __init__(self) -> None:
|
|
24
24
|
"""Initialize the status trait."""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None:
|
|
29
|
-
"""Update the trait from raw DPS data."""
|
|
30
|
-
if _CONVERTER.update_from_dps(self, decoded_dps):
|
|
31
|
-
self._notify_update()
|
|
25
|
+
Q10Status.__init__(self)
|
|
26
|
+
UpdatableTrait.__init__(self, command=None, logger=_LOGGER)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from roborock.data.b01_q10.b01_q10_code_mappings import (
|
|
4
4
|
B01_Q10_DP,
|
|
5
5
|
YXCleanType,
|
|
6
|
+
YXDeviceCleanTask,
|
|
6
7
|
YXFanLevel,
|
|
7
8
|
)
|
|
8
9
|
|
|
@@ -21,50 +22,77 @@ class VacuumTrait:
|
|
|
21
22
|
self._command = command
|
|
22
23
|
|
|
23
24
|
async def start_clean(self) -> None:
|
|
24
|
-
"""Start
|
|
25
|
+
"""Start a whole-home clean.
|
|
26
|
+
|
|
27
|
+
The ``dpStartClean`` (201) command selects a task by code: ``1`` =
|
|
28
|
+
whole-home, ``2`` = segment/room (see :meth:`clean_segments`), ``3`` =
|
|
29
|
+
zone, ``4`` = build map, ``5`` = spot. Whole-home and spot accept the
|
|
30
|
+
bare integer code; segment cleaning needs a room selection (an object
|
|
31
|
+
payload) instead.
|
|
32
|
+
|
|
33
|
+
Verified live against ss07 hardware: ``{"dps": {"201": 1}}`` starts a
|
|
34
|
+
whole-home clean (clean_task_type -> 1).
|
|
35
|
+
"""
|
|
36
|
+
await self._command.send(command=B01_Q10_DP.START_CLEAN, params=1)
|
|
37
|
+
|
|
38
|
+
async def clean_segments(self, segment_ids: list[int]) -> None:
|
|
39
|
+
"""Start a room / segment clean for the given segment (room) ids.
|
|
40
|
+
|
|
41
|
+
The ids are the same room ids the device reports on its map (see the Q10
|
|
42
|
+
``MapContentTrait`` -- ``map.rooms``, each with an ``id``).
|
|
43
|
+
|
|
44
|
+
Unlike whole-home and spot, ``dpStartClean`` (201) carries the room
|
|
45
|
+
selection as an object: ``{"cmd": <task>, "clean_paramters": [<id>, ...]}``,
|
|
46
|
+
where ``cmd`` is the segment-clean task code.
|
|
47
|
+
|
|
48
|
+
Verified live against ss07 hardware: sending
|
|
49
|
+
``{"dps": {"201": {"cmd": 2, "clean_paramters": [9]}}}`` starts cleaning
|
|
50
|
+
room 9 (clean_task_type -> 2 / electoral).
|
|
51
|
+
"""
|
|
25
52
|
await self._command.send(
|
|
26
53
|
command=B01_Q10_DP.START_CLEAN,
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
# 4 = fast create map
|
|
31
|
-
params={"cmd": 1},
|
|
54
|
+
# "clean_paramters" intentionally mirrors the device's misspelling of
|
|
55
|
+
# "parameters" -- the firmware only accepts that exact key.
|
|
56
|
+
params={"cmd": YXDeviceCleanTask.ELECTORAL.code, "clean_paramters": segment_ids},
|
|
32
57
|
)
|
|
33
58
|
|
|
59
|
+
async def spot_clean(self) -> None:
|
|
60
|
+
"""Start a spot / part clean around the robot's current position.
|
|
61
|
+
|
|
62
|
+
Verified live: ``{"dps": {"201": 5}}`` (clean_task_type -> 5).
|
|
63
|
+
"""
|
|
64
|
+
await self._command.send(command=B01_Q10_DP.START_CLEAN, params=5)
|
|
65
|
+
|
|
34
66
|
async def pause_clean(self) -> None:
|
|
35
|
-
"""Pause
|
|
36
|
-
await self._command.send(
|
|
37
|
-
command=B01_Q10_DP.PAUSE,
|
|
38
|
-
params={},
|
|
39
|
-
)
|
|
67
|
+
"""Pause the current task. Verified live: ``{"dps": {"204": 0}}``."""
|
|
68
|
+
await self._command.send(command=B01_Q10_DP.PAUSE, params=0)
|
|
40
69
|
|
|
41
70
|
async def resume_clean(self) -> None:
|
|
42
|
-
"""Resume
|
|
43
|
-
await self._command.send(
|
|
44
|
-
command=B01_Q10_DP.RESUME,
|
|
45
|
-
params={},
|
|
46
|
-
)
|
|
71
|
+
"""Resume a paused task. Verified live: ``{"dps": {"205": 0}}``."""
|
|
72
|
+
await self._command.send(command=B01_Q10_DP.RESUME, params=0)
|
|
47
73
|
|
|
48
74
|
async def stop_clean(self) -> None:
|
|
49
|
-
"""Stop
|
|
50
|
-
await self._command.send(
|
|
51
|
-
command=B01_Q10_DP.STOP,
|
|
52
|
-
params={},
|
|
53
|
-
)
|
|
75
|
+
"""Stop / cancel the current task. Verified live: ``{"dps": {"206": 0}}``."""
|
|
76
|
+
await self._command.send(command=B01_Q10_DP.STOP, params=0)
|
|
54
77
|
|
|
55
78
|
async def return_to_dock(self) -> None:
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
79
|
+
"""Send the robot back to the dock to charge.
|
|
80
|
+
|
|
81
|
+
Uses ``dpStartBack`` (202) with the back-dock task code ``5`` (charge),
|
|
82
|
+
matching the official app. Verified live: ``{"dps": {"202": 5}}`` puts the
|
|
83
|
+
robot into the returning state. (The other back-dock codes are ``1`` =
|
|
84
|
+
wash mop en route and ``4`` = collect dust en route.)
|
|
85
|
+
"""
|
|
86
|
+
await self._command.send(command=B01_Q10_DP.START_BACK, params=5)
|
|
61
87
|
|
|
62
88
|
async def empty_dustbin(self) -> None:
|
|
63
|
-
"""Empty the dustbin at the dock.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
"""Empty the dustbin at the dock.
|
|
90
|
+
|
|
91
|
+
Verified live: ``{"dps": {"203": 2}}`` triggers dust collection
|
|
92
|
+
(status -> emptying_the_bin). This is a dock task (``dpStartDockTask``),
|
|
93
|
+
distinct from the en-route collect-dust back-dock code.
|
|
94
|
+
"""
|
|
95
|
+
await self._command.send(command=B01_Q10_DP.START_DOCK_TASK, params=2)
|
|
68
96
|
|
|
69
97
|
async def set_clean_mode(self, mode: YXCleanType) -> None:
|
|
70
98
|
"""Set the cleaning mode (vacuum, mop, or both)."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Sound volume trait for Q10 B01 devices."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
6
|
+
from roborock.data.b01_q10.b01_q10_containers import SoundVolume
|
|
7
|
+
from roborock.devices.traits.common import DpsDataConverter
|
|
8
|
+
|
|
9
|
+
from .command import CommandTrait
|
|
10
|
+
from .common import UpdatableTrait
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SoundVolumeTrait(SoundVolume, UpdatableTrait):
|
|
16
|
+
"""Trait for reading and setting the speaker volume of a Q10 device."""
|
|
17
|
+
|
|
18
|
+
_CONVERTER = DpsDataConverter.from_dataclass(SoundVolume)
|
|
19
|
+
|
|
20
|
+
def __init__(self, command: CommandTrait) -> None:
|
|
21
|
+
"""Initialize the volume trait."""
|
|
22
|
+
SoundVolume.__init__(self)
|
|
23
|
+
UpdatableTrait.__init__(self, command, _LOGGER)
|
|
24
|
+
|
|
25
|
+
async def set_volume(self, volume: int) -> None:
|
|
26
|
+
"""Set the speaker volume (0-100)."""
|
|
27
|
+
if not 0 <= volume <= 100:
|
|
28
|
+
raise ValueError("volume must be between 0 and 100")
|
|
29
|
+
await self._write(B01_Q10_DP.VOLUME, volume)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|