python-roborock 4.26.3__tar.gz → 5.1.0__tar.gz

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.
Files changed (103) hide show
  1. {python_roborock-4.26.3 → python_roborock-5.1.0}/PKG-INFO +3 -2
  2. {python_roborock-4.26.3 → python_roborock-5.1.0}/pyproject.toml +9 -2
  3. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/broadcast_protocol.py +0 -2
  4. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/cli.py +6 -1
  5. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +23 -23
  6. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/code_mappings.py +6 -8
  7. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/v1/v1_code_mappings.py +3 -1
  8. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/device_features.py +2 -4
  9. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/device_manager.py +1 -1
  10. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/rpc/b01_q10_channel.py +0 -2
  11. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/rpc/b01_q7_channel.py +4 -3
  12. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/b01/q7/__init__.py +28 -8
  13. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -2
  14. python_roborock-5.1.0/roborock/devices/traits/b01/q7/map_content.py +98 -0
  15. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/network_info.py +0 -2
  16. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/diagnostics.py +4 -6
  17. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/exceptions.py +0 -2
  18. python_roborock-5.1.0/roborock/map/b01_map_parser.py +178 -0
  19. python_roborock-5.1.0/roborock/map/proto/__init__.py +1 -0
  20. python_roborock-5.1.0/roborock/map/proto/b01_scmap.proto +70 -0
  21. python_roborock-5.1.0/roborock/map/proto/b01_scmap_pb2.py +48 -0
  22. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/protocol.py +0 -2
  23. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/roborock_message.py +3 -4
  24. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/roborock_typing.py +2 -3
  25. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/util.py +0 -2
  26. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/web_api.py +0 -2
  27. {python_roborock-4.26.3 → python_roborock-5.1.0}/.gitignore +0 -0
  28. {python_roborock-4.26.3 → python_roborock-5.1.0}/LICENSE +0 -0
  29. {python_roborock-4.26.3 → python_roborock-5.1.0}/README.md +0 -0
  30. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/__init__.py +0 -0
  31. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/callbacks.py +0 -0
  32. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/const.py +0 -0
  33. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/__init__.py +0 -0
  34. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/b01_q10/__init__.py +0 -0
  35. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  36. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/b01_q7/__init__.py +0 -0
  37. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  38. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  39. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/containers.py +0 -0
  40. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/dyad/__init__.py +0 -0
  41. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  42. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/dyad/dyad_containers.py +0 -0
  43. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/v1/__init__.py +0 -0
  44. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  45. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/v1/v1_containers.py +0 -0
  46. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/zeo/__init__.py +0 -0
  47. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  48. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/data/zeo/zeo_containers.py +0 -0
  49. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/README.md +0 -0
  50. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/__init__.py +0 -0
  51. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/cache.py +0 -0
  52. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/device.py +0 -0
  53. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/file_cache.py +0 -0
  54. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/rpc/__init__.py +0 -0
  55. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/rpc/a01_channel.py +0 -0
  56. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/rpc/v1_channel.py +0 -0
  57. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/__init__.py +0 -0
  58. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/a01/__init__.py +0 -0
  59. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/b01/__init__.py +0 -0
  60. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  61. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  62. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/b01/q10/common.py +0 -0
  63. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/b01/q10/status.py +0 -0
  64. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  65. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/b01/q7/map.py +0 -0
  66. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/traits_mixin.py +0 -0
  67. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/__init__.py +0 -0
  68. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  69. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  70. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/command.py +0 -0
  71. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/common.py +0 -0
  72. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  73. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/device_features.py +0 -0
  74. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  75. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  76. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  77. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/home.py +0 -0
  78. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/led_status.py +0 -0
  79. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/map_content.py +0 -0
  80. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/maps.py +0 -0
  81. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/rooms.py +0 -0
  82. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/routines.py +0 -0
  83. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  84. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/status.py +0 -0
  85. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  86. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/volume.py +0 -0
  87. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  88. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/transport/__init__.py +0 -0
  89. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/transport/channel.py +0 -0
  90. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/transport/local_channel.py +0 -0
  91. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  92. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/map/__init__.py +0 -0
  93. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/map/map_parser.py +0 -0
  94. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/mqtt/__init__.py +0 -0
  95. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/mqtt/health_manager.py +0 -0
  96. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/mqtt/roborock_session.py +0 -0
  97. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/mqtt/session.py +0 -0
  98. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/protocols/__init__.py +0 -0
  99. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/protocols/a01_protocol.py +0 -0
  100. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  101. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  102. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/protocols/v1_protocol.py +0 -0
  103. {python_roborock-4.26.3 → python_roborock-5.1.0}/roborock/py.typed +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 4.26.3
3
+ Version: 5.1.0
4
4
  Summary: A package to control Roborock vacuums.
5
- Project-URL: Repository, https://github.com/humbertogontijo/python-roborock
5
+ Project-URL: Repository, https://github.com/python-roborock/python-roborock
6
6
  Project-URL: Documentation, https://python-roborock.readthedocs.io/
7
7
  Author: Lash-L, allenporter
8
8
  Author-email: humbertogontijo <humbertogontijo@users.noreply.github.com>
@@ -21,6 +21,7 @@ Requires-Dist: click-shell~=2.1
21
21
  Requires-Dist: click>=8
22
22
  Requires-Dist: construct<3,>=2.10.57
23
23
  Requires-Dist: paho-mqtt<3.0.0,>=1.6.1
24
+ Requires-Dist: protobuf<7,>=5
24
25
  Requires-Dist: pycryptodomex~=3.18; sys_platform == 'darwin'
25
26
  Requires-Dist: pycryptodome~=3.18
26
27
  Requires-Dist: pyrate-limiter<5,>=4.0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-roborock"
3
- version = "4.26.3"
3
+ version = "5.1.0"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
6
6
  requires-python = ">=3.11, <4"
@@ -25,6 +25,7 @@ dependencies = [
25
25
  "pycryptodomex~=3.18 ; sys_platform == 'darwin'",
26
26
  "paho-mqtt>=1.6.1,<3.0.0",
27
27
  "construct>=2.10.57,<3",
28
+ "protobuf>=5,<7",
28
29
  "vacuum-map-parser-roborock",
29
30
  "pyrate-limiter>=4.0.0,<5",
30
31
  "aiomqtt>=2.5.0,<3",
@@ -32,7 +33,7 @@ dependencies = [
32
33
  ]
33
34
 
34
35
  [project.urls]
35
- Repository = "https://github.com/humbertogontijo/python-roborock"
36
+ Repository = "https://github.com/python-roborock/python-roborock"
36
37
  Documentation = "https://python-roborock.readthedocs.io/"
37
38
 
38
39
  [project.scripts]
@@ -97,9 +98,15 @@ major_tags= ["refactor"]
97
98
  lint.ignore = ["F403", "E741"]
98
99
  lint.select=["E", "F", "UP", "I"]
99
100
  line-length = 120
101
+ extend-exclude = ["roborock/map/proto/*_pb2.py"]
100
102
 
101
103
  [tool.ruff.lint.per-file-ignores]
102
104
  "*/__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
103
110
 
104
111
  [tool.pytest.ini_options]
105
112
  asyncio_mode = "auto"
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import asyncio
4
2
  import hashlib
5
3
  import json
@@ -1275,7 +1275,12 @@ async def q10_empty_dustbin(ctx: click.Context, device_id: str) -> None:
1275
1275
 
1276
1276
  @session.command()
1277
1277
  @click.option("--device_id", required=True, help="Device ID")
1278
- @click.option("--mode", required=True, type=click.Choice(["bothwork", "onlysweep", "onlymop"]), help="Clean mode")
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
+ )
1279
1284
  @click.pass_context
1280
1285
  @async_command
1281
1286
  async def q10_set_clean_mode(ctx: click.Context, device_id: str, mode: str) -> None:
@@ -136,9 +136,9 @@ class YXFanLevel(RoborockModeEnum):
136
136
 
137
137
  class YXWaterLevel(RoborockModeEnum):
138
138
  UNKNOWN = "unknown", -1
139
- CLOSE = "close", 0
139
+ OFF = "off", 0 # close
140
140
  LOW = "low", 1
141
- MIDDLE = "middle", 2
141
+ MEDIUM = "medium", 2 # middle
142
142
  HIGH = "high", 3
143
143
 
144
144
 
@@ -157,31 +157,31 @@ class YXRoomMaterial(RoborockModeEnum):
157
157
 
158
158
  class YXCleanType(RoborockModeEnum):
159
159
  UNKNOWN = "unknown", -1
160
- BOTH_WORK = "bothwork", 1
161
- ONLY_SWEEP = "onlysweep", 2
162
- ONLY_MOP = "onlymop", 3
160
+ VAC_AND_MOP = "vac_and_mop", 1 # bothwork
161
+ VACUUM = "vacuum", 2 # onlysweep
162
+ MOP = "mop", 3 # onlymop
163
163
 
164
164
 
165
165
  class YXDeviceState(RoborockModeEnum):
166
166
  UNKNOWN = "unknown", -1
167
- SLEEP_STATE = "sleeping", 2
168
- STANDBY_STATE = "standby", 3
169
- CLEANING_STATE = "cleaning", 5
170
- TO_CHARGE_STATE = "going_to_charge", 6
171
- REMOTEING_STATE = "remote_control", 7
172
- CHARGING_STATE = "charging", 8
173
- PAUSE_STATE = "paused", 10
174
- FAULT_STATE = "fault", 12
175
- UPGRADE_STATE = "updating", 14
176
- DUSTING = "dusting", 22
177
- CREATING_MAP_STATE = "creating_map", 29
178
- MAP_SAVE_STATE = "saving_map", 99
179
- RE_LOCATION_STATE = "relocating", 101
180
- ROBOT_SWEEPING = "sweeping", 102
181
- ROBOT_MOPING = "mopping", 103
182
- ROBOT_SWEEP_AND_MOPING = "sweep_and_mop", 104
183
- ROBOT_TRANSITIONING = "transitioning", 105
184
- ROBOT_WAIT_CHARGE = "waiting_to_charge", 108
167
+ SLEEPING = "sleeping", 2 # sleepstate
168
+ IDLE = "idle", 3 # standbystate
169
+ CLEANING = "cleaning", 5 # cleaningstate
170
+ RETURNING_HOME = "returning_home", 6 # tochargestate
171
+ REMOTE_CONTROL_ACTIVE = "remote_control_active", 7 # remoteingstate
172
+ CHARGING = "charging", 8 # chargingstate
173
+ PAUSED = "paused", 10 # pausestate
174
+ ERROR = "error", 12 # faultstate
175
+ UPDATING = "updating", 14 # upgradestate
176
+ EMPTYING_THE_BIN = "emptying_the_bin", 22 # dusting
177
+ MAPPING = "mapping", 29 # creatingmapstate
178
+ SAVING_MAP = "saving_map", 99 # mapsavestate
179
+ RELOCATING = "relocating", 101 # relocationstate
180
+ SWEEPING = "sweeping", 102 # robotsweeping
181
+ MOPPING = "mopping", 103 # robotmoping
182
+ SWEEP_AND_MOP = "sweep_and_mop", 104 # robotsweepandmoping
183
+ TRANSITIONING = "transitioning", 105 # robottransitioning
184
+ WAITING_TO_CHARGE = "waiting_to_charge", 108 # robotwaitcharge
185
185
 
186
186
 
187
187
  class YXBackType(RoborockModeEnum):
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import logging
4
2
  from collections import namedtuple
5
3
  from enum import Enum, IntEnum, StrEnum
@@ -17,7 +15,7 @@ class RoborockEnum(IntEnum):
17
15
  return super().name.lower()
18
16
 
19
17
  @classmethod
20
- def _missing_(cls: type[RoborockEnum], key) -> RoborockEnum:
18
+ def _missing_(cls: type[Self], key) -> Self:
21
19
  if hasattr(cls, "unknown"):
22
20
  warning = f"Missing {cls.__name__} code: {key} - defaulting to 'unknown'"
23
21
  if warning not in completed_warnings:
@@ -32,23 +30,23 @@ class RoborockEnum(IntEnum):
32
30
  return default_value
33
31
 
34
32
  @classmethod
35
- def as_dict(cls: type[RoborockEnum]):
33
+ def as_dict(cls: type[Self]):
36
34
  return {i.name: i.value for i in cls if i.name != "missing"}
37
35
 
38
36
  @classmethod
39
- def as_enum_dict(cls: type[RoborockEnum]):
37
+ def as_enum_dict(cls: type[Self]):
40
38
  return {i.value: i for i in cls if i.name != "missing"}
41
39
 
42
40
  @classmethod
43
- def values(cls: type[RoborockEnum]) -> list[int]:
41
+ def values(cls: type[Self]) -> list[int]:
44
42
  return list(cls.as_dict().values())
45
43
 
46
44
  @classmethod
47
- def keys(cls: type[RoborockEnum]) -> list[str]:
45
+ def keys(cls: type[Self]) -> list[str]:
48
46
  return list(cls.as_dict().keys())
49
47
 
50
48
  @classmethod
51
- def items(cls: type[RoborockEnum]):
49
+ def items(cls: type[Self]):
52
50
  return cls.as_dict().items()
53
51
 
54
52
 
@@ -1,3 +1,5 @@
1
+ from typing import Self
2
+
1
3
  from ..code_mappings import RoborockEnum
2
4
 
3
5
 
@@ -91,7 +93,7 @@ class RoborockStartType(RoborockEnum):
91
93
 
92
94
  class RoborockDssCodes(RoborockEnum):
93
95
  @classmethod
94
- def _missing_(cls: type[RoborockEnum], key) -> RoborockEnum:
96
+ def _missing_(cls: type[Self], key) -> Self:
95
97
  # If the calculated value is not provided, then it should be viewed as okay.
96
98
  # As the math will sometimes result in you getting numbers that don't matter.
97
99
  return cls.okay # type: ignore
@@ -1,8 +1,6 @@
1
- from __future__ import annotations
2
-
3
1
  from dataclasses import dataclass, field, fields
4
2
  from enum import IntEnum, StrEnum
5
- from typing import Any
3
+ from typing import Any, Self
6
4
 
7
5
  from roborock.data.code_mappings import RoborockProductNickname
8
6
  from roborock.data.containers import RoborockBase
@@ -566,7 +564,7 @@ class DeviceFeatures(RoborockBase):
566
564
  new_feature_info_str: str,
567
565
  feature_info: list[int],
568
566
  product_nickname: RoborockProductNickname | None,
569
- ) -> DeviceFeatures:
567
+ ) -> Self:
570
568
  """Creates a DeviceFeatures instance from raw feature flags.
571
569
  :param new_feature_info: A int from get_init_status (sometimes can be found in homedata, but it is not always)
572
570
  :param new_feature_info_str: A hex string from get_init_status or home_data.
@@ -251,7 +251,7 @@ async def create_device_manager(
251
251
  trait = b01.q10.create(channel)
252
252
  elif "sc" in model_part:
253
253
  # Q7 devices start with 'sc' in their model naming.
254
- trait = b01.q7.create(channel)
254
+ trait = b01.q7.create(product, device, channel)
255
255
  else:
256
256
  raise UnsupportedDeviceError(f"Device {device.name} has unsupported B01 model: {product.model}")
257
257
  case _:
@@ -1,7 +1,5 @@
1
1
  """Thin wrapper around the MQTT channel for Roborock B01 Q10 devices."""
2
2
 
3
- from __future__ import annotations
4
-
5
3
  import logging
6
4
  from collections.abc import AsyncGenerator
7
5
  from typing import Any
@@ -6,7 +6,7 @@ import asyncio
6
6
  import json
7
7
  import logging
8
8
  from collections.abc import Callable
9
- from typing import Any, TypeVar
9
+ from typing import TypeAlias, TypeVar
10
10
 
11
11
  from roborock.devices.transport.mqtt_channel import MqttChannel
12
12
  from roborock.exceptions import RoborockException
@@ -16,6 +16,7 @@ from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
16
16
  _LOGGER = logging.getLogger(__name__)
17
17
  _TIMEOUT = 10.0
18
18
  _T = TypeVar("_T")
19
+ DecodedB01Response: TypeAlias = dict[str, object] | str
19
20
 
20
21
 
21
22
  def _matches_map_response(response_message: RoborockMessage, *, version: bytes | None) -> bytes | None:
@@ -61,11 +62,11 @@ async def _send_command(
61
62
  async def send_decoded_command(
62
63
  mqtt_channel: MqttChannel,
63
64
  request_message: Q7RequestMessage,
64
- ) -> Any:
65
+ ) -> DecodedB01Response:
65
66
  """Send a command on the MQTT channel and get a decoded response."""
66
67
  _LOGGER.debug("Sending B01 MQTT command: %s", request_message)
67
68
 
68
- def find_response(response_message: RoborockMessage) -> Any | None:
69
+ def find_response(response_message: RoborockMessage) -> DecodedB01Response | None:
69
70
  """Handle incoming messages and resolve the future."""
70
71
  try:
71
72
  decoded_dps = decode_rpc_response(response_message)
@@ -1,10 +1,14 @@
1
1
  """Traits for Q7 B01 devices.
2
- Potentially other devices may fall into this category in the future."""
2
+
3
+ Potentially other devices may fall into this category in the future.
4
+ """
5
+
6
+ from __future__ import annotations
3
7
 
4
8
  from typing import Any
5
9
 
6
10
  from roborock import B01Props
7
- from roborock.data import Q7MapList, Q7MapListEntry
11
+ from roborock.data import HomeDataDevice, HomeDataProduct, Q7MapList, Q7MapListEntry
8
12
  from roborock.data.b01_q7.b01_q7_code_mappings import (
9
13
  CleanPathPreferenceMapping,
10
14
  CleanRepeatMapping,
@@ -23,18 +27,20 @@ from roborock.roborock_typing import RoborockB01Q7Methods
23
27
 
24
28
  from .clean_summary import CleanSummaryTrait
25
29
  from .map import MapTrait
30
+ from .map_content import MapContentTrait
26
31
 
27
32
  __all__ = [
28
33
  "Q7PropertiesApi",
29
34
  "CleanSummaryTrait",
30
35
  "MapTrait",
36
+ "MapContentTrait",
31
37
  "Q7MapList",
32
38
  "Q7MapListEntry",
33
39
  ]
34
40
 
35
41
 
36
42
  class Q7PropertiesApi(Trait):
37
- """API for interacting with B01 devices."""
43
+ """API for interacting with B01 Q7 devices."""
38
44
 
39
45
  clean_summary: CleanSummaryTrait
40
46
  """Trait for clean records / clean summary (Q7 `service.get_record_list`)."""
@@ -42,11 +48,25 @@ class Q7PropertiesApi(Trait):
42
48
  map: MapTrait
43
49
  """Trait for map list metadata + raw map payload retrieval."""
44
50
 
45
- def __init__(self, channel: MqttChannel) -> None:
46
- """Initialize the B01Props API."""
51
+ map_content: MapContentTrait
52
+ """Trait for fetching parsed current map content."""
53
+
54
+ def __init__(self, channel: MqttChannel, *, device: HomeDataDevice, product: HomeDataProduct) -> None:
55
+ """Initialize the Q7 API."""
47
56
  self._channel = channel
57
+ self._device = device
58
+ self._product = product
59
+
60
+ if not device.sn or not product.model:
61
+ raise ValueError("B01 Q7 map content requires device serial number and product model metadata")
62
+
48
63
  self.clean_summary = CleanSummaryTrait(channel)
49
64
  self.map = MapTrait(channel)
65
+ self.map_content = MapContentTrait(
66
+ self.map,
67
+ serial=device.sn,
68
+ model=product.model,
69
+ )
50
70
 
51
71
  async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
52
72
  """Query the device for the values of the given Q7 properties."""
@@ -151,6 +171,6 @@ class Q7PropertiesApi(Trait):
151
171
  )
152
172
 
153
173
 
154
- def create(channel: MqttChannel) -> Q7PropertiesApi:
155
- """Create traits for B01 devices."""
156
- return Q7PropertiesApi(channel)
174
+ def create(product: HomeDataProduct, device: HomeDataDevice, channel: MqttChannel) -> Q7PropertiesApi:
175
+ """Create traits for B01 Q7 devices."""
176
+ return Q7PropertiesApi(channel, device=device, product=product)
@@ -4,8 +4,6 @@ For B01/Q7, the Roborock app uses `service.get_record_list` which returns totals
4
4
  and a `record_list` whose items contain a JSON string in `detail`.
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
7
  import logging
10
8
 
11
9
  from roborock import CleanRecordDetail, CleanRecordList, CleanRecordSummary
@@ -0,0 +1,98 @@
1
+ """Trait for fetching parsed map content from B01/Q7 devices.
2
+
3
+ This intentionally mirrors the v1 `MapContentTrait` contract:
4
+ - `refresh()` performs I/O and populates cached fields
5
+ - `parse_map_content()` reparses cached raw bytes without I/O
6
+ - fields `image_content`, `map_data`, and `raw_api_response` are then readable
7
+
8
+ For B01/Q7 devices, the underlying raw map payload is retrieved via `MapTrait`.
9
+ """
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from vacuum_map_parser_base.map_data import MapData
14
+
15
+ from roborock.data import RoborockBase
16
+ from roborock.devices.traits import Trait
17
+ from roborock.exceptions import RoborockException
18
+ from roborock.map.b01_map_parser import B01MapParser, B01MapParserConfig
19
+
20
+ from .map import MapTrait
21
+
22
+ _TRUNCATE_LENGTH = 20
23
+
24
+
25
+ @dataclass
26
+ class MapContent(RoborockBase):
27
+ """Dataclass representing map content."""
28
+
29
+ image_content: bytes | None = None
30
+ """The rendered image of the map in PNG format."""
31
+
32
+ map_data: MapData | None = None
33
+ """Parsed map data (metadata for points on the map)."""
34
+
35
+ raw_api_response: bytes | None = None
36
+ """Raw bytes of the map payload from the device.
37
+
38
+ This should be treated as an opaque blob used only internally by this
39
+ library to re-parse the map data when needed.
40
+ """
41
+
42
+ def __repr__(self) -> str:
43
+ img = self.image_content
44
+ if img and len(img) > _TRUNCATE_LENGTH:
45
+ img = img[: _TRUNCATE_LENGTH - 3] + b"..."
46
+ return f"MapContent(image_content={img!r}, map_data={self.map_data!r})"
47
+
48
+
49
+ class MapContentTrait(MapContent, Trait):
50
+ """Trait for fetching parsed map content for Q7 devices."""
51
+
52
+ def __init__(
53
+ self,
54
+ map_trait: MapTrait,
55
+ *,
56
+ serial: str,
57
+ model: str,
58
+ map_parser_config: B01MapParserConfig | None = None,
59
+ ) -> None:
60
+ super().__init__()
61
+ self._map_trait = map_trait
62
+ self._serial = serial
63
+ self._model = model
64
+ self._map_parser = B01MapParser(map_parser_config)
65
+
66
+ async def refresh(self) -> None:
67
+ """Fetch, decode, and parse the current map payload."""
68
+ raw_payload = await self._map_trait.get_current_map_payload()
69
+ parsed = self.parse_map_content(raw_payload)
70
+ self.image_content = parsed.image_content
71
+ self.map_data = parsed.map_data
72
+ self.raw_api_response = parsed.raw_api_response
73
+
74
+ def parse_map_content(self, response: bytes) -> MapContent:
75
+ """Parse map content from raw bytes.
76
+
77
+ This mirrors the v1 trait behavior so cached map payload bytes can be
78
+ reparsed without going back to the device.
79
+ """
80
+ try:
81
+ parsed_data = self._map_parser.parse(
82
+ response,
83
+ serial=self._serial,
84
+ model=self._model,
85
+ )
86
+ except RoborockException:
87
+ raise
88
+ except Exception as ex:
89
+ raise RoborockException("Failed to parse B01 map data") from ex
90
+
91
+ if parsed_data.image_content is None:
92
+ raise RoborockException("Failed to render B01 map image")
93
+
94
+ return MapContent(
95
+ image_content=parsed_data.image_content,
96
+ map_data=parsed_data.map_data,
97
+ raw_api_response=response,
98
+ )
@@ -1,7 +1,5 @@
1
1
  """Trait for device network information."""
2
2
 
3
- from __future__ import annotations
4
-
5
3
  import logging
6
4
 
7
5
  from roborock.data import NetworkInfo
@@ -9,13 +9,11 @@ data is collected and exposed to clients via higher level APIs like the
9
9
  DeviceManager.
10
10
  """
11
11
 
12
- from __future__ import annotations
13
-
14
12
  import time
15
13
  from collections import Counter
16
14
  from collections.abc import Generator, Mapping
17
15
  from contextlib import contextmanager
18
- from typing import Any, TypeVar, cast
16
+ from typing import Any, Self, TypeVar, cast
19
17
 
20
18
 
21
19
  class Diagnostics:
@@ -28,7 +26,7 @@ class Diagnostics:
28
26
  def __init__(self) -> None:
29
27
  """Initialize Diagnostics."""
30
28
  self._counter: Counter = Counter()
31
- self._subkeys: dict[str, Diagnostics] = {}
29
+ self._subkeys: dict[str, Self] = {}
32
30
 
33
31
  def increment(self, key: str, count: int = 1) -> None:
34
32
  """Increment a counter for the specified key/event."""
@@ -49,7 +47,7 @@ class Diagnostics:
49
47
  data[k] = v
50
48
  return data
51
49
 
52
- def subkey(self, key: str) -> Diagnostics:
50
+ def subkey(self, key: str) -> Self:
53
51
  """Return sub-Diagnostics object with the specified subkey.
54
52
 
55
53
  This will create a new Diagnostics object if one does not already exist
@@ -63,7 +61,7 @@ class Diagnostics:
63
61
  The Diagnostics object for the specified subkey.
64
62
  """
65
63
  if key not in self._subkeys:
66
- self._subkeys[key] = Diagnostics()
64
+ self._subkeys[key] = type(self)()
67
65
  return self._subkeys[key]
68
66
 
69
67
  @contextmanager
@@ -1,7 +1,5 @@
1
1
  """Roborock exceptions."""
2
2
 
3
- from __future__ import annotations
4
-
5
3
 
6
4
  class RoborockException(Exception):
7
5
  """Class for Roborock exceptions."""
@@ -0,0 +1,178 @@
1
+ """Module for parsing B01/Q7 map content.
2
+
3
+ Observed Q7 `MAP_RESPONSE` payloads follow this decode pipeline:
4
+ - base64-encoded ASCII
5
+ - AES-ECB encrypted with the derived map key
6
+ - PKCS7 padded
7
+ - ASCII hex for a zlib-compressed SCMap payload
8
+
9
+ The inner SCMap blob is parsed with protobuf messages generated from
10
+ `roborock/map/proto/b01_scmap.proto`.
11
+ """
12
+
13
+ import base64
14
+ import binascii
15
+ import hashlib
16
+ import io
17
+ import zlib
18
+ from dataclasses import dataclass
19
+
20
+ from Crypto.Cipher import AES
21
+ from google.protobuf.message import DecodeError, Message
22
+ from PIL import Image
23
+ from vacuum_map_parser_base.config.image_config import ImageConfig
24
+ from vacuum_map_parser_base.map_data import ImageData, MapData
25
+
26
+ from roborock.exceptions import RoborockException
27
+ from roborock.map.proto.b01_scmap_pb2 import RobotMap # type: ignore[attr-defined]
28
+ from roborock.protocol import Utils
29
+
30
+ from .map_parser import ParsedMapData
31
+
32
+ _MAP_FILE_FORMAT = "PNG"
33
+
34
+
35
+ @dataclass
36
+ class B01MapParserConfig:
37
+ """Configuration for the B01/Q7 map parser."""
38
+
39
+ map_scale: int = 4
40
+ """Scale factor for the rendered map image."""
41
+
42
+
43
+ class B01MapParser:
44
+ """Decoder/parser for B01/Q7 SCMap payloads."""
45
+
46
+ def __init__(self, config: B01MapParserConfig | None = None) -> None:
47
+ self._config = config or B01MapParserConfig()
48
+
49
+ def parse(self, raw_payload: bytes, *, serial: str, model: str) -> ParsedMapData:
50
+ """Parse a raw MAP_RESPONSE payload and return a PNG + MapData."""
51
+ inflated = _decode_b01_map_payload(raw_payload, serial=serial, model=model)
52
+ parsed = _parse_scmap_payload(inflated)
53
+ size_x, size_y, grid = _extract_grid(parsed)
54
+ room_names = _extract_room_names(parsed)
55
+
56
+ image = _render_occupancy_image(grid, size_x=size_x, size_y=size_y, scale=self._config.map_scale)
57
+
58
+ map_data = MapData()
59
+ map_data.image = ImageData(
60
+ size=size_x * size_y,
61
+ top=0,
62
+ left=0,
63
+ height=size_y,
64
+ width=size_x,
65
+ image_config=ImageConfig(scale=self._config.map_scale),
66
+ data=image,
67
+ img_transformation=lambda p: p,
68
+ )
69
+ if room_names:
70
+ map_data.additional_parameters["room_names"] = room_names
71
+
72
+ image_bytes = io.BytesIO()
73
+ image.save(image_bytes, format=_MAP_FILE_FORMAT)
74
+
75
+ return ParsedMapData(
76
+ image_content=image_bytes.getvalue(),
77
+ map_data=map_data,
78
+ )
79
+
80
+
81
+ def _derive_map_key(serial: str, model: str) -> bytes:
82
+ """Derive the B01/Q7 map decrypt key from serial + model."""
83
+ model_suffix = model.split(".")[-1]
84
+ model_key = (model_suffix + "0" * 16)[:16].encode()
85
+ material = f"{serial}+{model_suffix}+{serial}".encode()
86
+ encrypted = Utils.encrypt_ecb(material, model_key)
87
+ md5 = hashlib.md5(base64.b64encode(encrypted), usedforsecurity=False).hexdigest()
88
+ return md5[8:24].encode()
89
+
90
+
91
+ def _decode_base64_payload(raw_payload: bytes) -> bytes:
92
+ blob = raw_payload.strip()
93
+ padded = blob + b"=" * (-len(blob) % 4)
94
+ try:
95
+ return base64.b64decode(padded, validate=True)
96
+ except binascii.Error as err:
97
+ raise RoborockException("Failed to decode B01 map payload") from err
98
+
99
+
100
+ def _decode_b01_map_payload(raw_payload: bytes, *, serial: str, model: str) -> bytes:
101
+ """Decode raw B01 `MAP_RESPONSE` payload into inflated SCMap bytes."""
102
+ # TODO: Move this lower-level B01 transport decode under `roborock.protocols`
103
+ # so this module only handles SCMap parsing/rendering.
104
+ encrypted_payload = _decode_base64_payload(raw_payload)
105
+ if len(encrypted_payload) % AES.block_size != 0:
106
+ raise RoborockException("Unexpected encrypted B01 map payload length")
107
+
108
+ map_key = _derive_map_key(serial, model)
109
+
110
+ try:
111
+ compressed_hex = Utils.decrypt_ecb(encrypted_payload, map_key).decode("ascii")
112
+ compressed_payload = bytes.fromhex(compressed_hex)
113
+ return zlib.decompress(compressed_payload)
114
+ except (ValueError, UnicodeDecodeError, zlib.error) as err:
115
+ raise RoborockException("Failed to decode B01 map payload") from err
116
+
117
+
118
+ def _parse_proto(blob: bytes, message: Message, *, context: str) -> None:
119
+ try:
120
+ message.ParseFromString(blob)
121
+ except DecodeError as err:
122
+ raise RoborockException(f"Failed to parse {context}") from err
123
+
124
+
125
+ def _parse_scmap_payload(payload: bytes) -> RobotMap:
126
+ """Parse inflated SCMap bytes into a generated protobuf message."""
127
+ parsed = RobotMap()
128
+ _parse_proto(payload, parsed, context="B01 SCMap")
129
+ return parsed
130
+
131
+
132
+ def _extract_grid(parsed: RobotMap) -> tuple[int, int, bytes]:
133
+ if not parsed.HasField("mapHead") or not parsed.HasField("mapData"):
134
+ raise RoborockException("Failed to parse B01 map header/grid")
135
+
136
+ size_x = parsed.mapHead.sizeX if parsed.mapHead.HasField("sizeX") else 0
137
+ size_y = parsed.mapHead.sizeY if parsed.mapHead.HasField("sizeY") else 0
138
+ if not size_x or not size_y or not parsed.mapData.HasField("mapData"):
139
+ raise RoborockException("Failed to parse B01 map header/grid")
140
+
141
+ map_data = parsed.mapData.mapData
142
+ expected_len = size_x * size_y
143
+ if len(map_data) < expected_len:
144
+ raise RoborockException("B01 map data shorter than expected dimensions")
145
+
146
+ return size_x, size_y, map_data[:expected_len]
147
+
148
+
149
+ def _extract_room_names(parsed: RobotMap) -> dict[int, str]:
150
+ # Expose room id/name mapping without inventing room geometry/polygons.
151
+ room_names: dict[int, str] = {}
152
+ for room in parsed.roomDataInfo:
153
+ if room.HasField("roomId"):
154
+ room_id = room.roomId
155
+ room_names[room_id] = room.roomName if room.HasField("roomName") else f"Room {room_id}"
156
+ return room_names
157
+
158
+
159
+ def _render_occupancy_image(grid: bytes, *, size_x: int, size_y: int, scale: int) -> Image.Image:
160
+ """Render the B01 occupancy grid into a simple image."""
161
+
162
+ # The observed occupancy grid contains only:
163
+ # - 0: outside/unknown
164
+ # - 127: wall/obstacle
165
+ # - 128: floor/free
166
+ table = bytearray(range(256))
167
+ table[0] = 0
168
+ table[127] = 180
169
+ table[128] = 255
170
+
171
+ mapped = grid.translate(bytes(table))
172
+ img = Image.frombytes("L", (size_x, size_y), mapped)
173
+ img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM).convert("RGB")
174
+
175
+ if scale > 1:
176
+ img = img.resize((size_x * scale, size_y * scale), resample=Image.Resampling.NEAREST)
177
+
178
+ return img
@@ -0,0 +1 @@
1
+ """Generated protobuf modules for Roborock map payloads."""
@@ -0,0 +1,70 @@
1
+ // Checked-in B01/Q7 SCMap schema for the generated runtime protobuf module.
2
+ // Regenerate the checked-in Python module after edits with:
3
+ // python -m grpc_tools.protoc -I./roborock/map/proto --python_out=./roborock/map/proto roborock/map/proto/b01_scmap.proto
4
+ // The generated file `b01_scmap_pb2.py` is checked in for runtime use and should
5
+ // not be edited by hand.
6
+ syntax = "proto2";
7
+
8
+ package b01.scmap;
9
+
10
+ message DevicePointInfo {
11
+ optional float x = 1;
12
+ optional float y = 2;
13
+ }
14
+
15
+ message MapBoundaryInfo {
16
+ optional string mapMd5 = 1;
17
+ optional uint32 vMinX = 2;
18
+ optional uint32 vMaxX = 3;
19
+ optional uint32 vMinY = 4;
20
+ optional uint32 vMaxY = 5;
21
+ }
22
+
23
+ message MapExtInfo {
24
+ optional uint32 taskBeginDate = 1;
25
+ optional uint32 mapUploadDate = 2;
26
+ optional uint32 mapValid = 3;
27
+ optional uint32 radian = 4;
28
+ optional uint32 force = 5;
29
+ optional uint32 cleanPath = 6;
30
+ optional MapBoundaryInfo boudaryInfo = 7;
31
+ optional uint32 mapVersion = 8;
32
+ optional uint32 mapValueType = 9;
33
+ }
34
+
35
+ message MapHeadInfo {
36
+ optional uint32 mapHeadId = 1;
37
+ optional uint32 sizeX = 2;
38
+ optional uint32 sizeY = 3;
39
+ optional float minX = 4;
40
+ optional float minY = 5;
41
+ optional float maxX = 6;
42
+ optional float maxY = 7;
43
+ optional float resolution = 8;
44
+ }
45
+
46
+ message MapDataInfo {
47
+ optional bytes mapData = 1;
48
+ }
49
+
50
+ message RoomDataInfo {
51
+ optional uint32 roomId = 1;
52
+ optional string roomName = 2;
53
+ optional uint32 roomTypeId = 3;
54
+ optional uint32 meterialId = 4;
55
+ optional uint32 cleanState = 5;
56
+ optional uint32 roomClean = 6;
57
+ optional uint32 roomCleanIndex = 7;
58
+ optional DevicePointInfo roomNamePost = 8;
59
+ optional uint32 colorId = 10;
60
+ optional uint32 floor_direction = 11;
61
+ optional uint32 global_seq = 12;
62
+ }
63
+
64
+ message RobotMap {
65
+ optional uint32 mapType = 1;
66
+ optional MapExtInfo mapExtInfo = 2;
67
+ optional MapHeadInfo mapHead = 3;
68
+ optional MapDataInfo mapData = 4;
69
+ repeated RoomDataInfo roomDataInfo = 12;
70
+ }
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: roborock/map/proto/b01_scmap.proto
5
+ # Protobuf Python Version: 6.31.1
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 31,
16
+ 1,
17
+ '',
18
+ 'roborock/map/proto/b01_scmap.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"roborock/map/proto/b01_scmap.proto\x12\tb01.scmap\"\'\n\x0f\x44\x65vicePointInfo\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\"]\n\x0fMapBoundaryInfo\x12\x0e\n\x06mapMd5\x18\x01 \x01(\t\x12\r\n\x05vMinX\x18\x02 \x01(\r\x12\r\n\x05vMaxX\x18\x03 \x01(\r\x12\r\n\x05vMinY\x18\x04 \x01(\r\x12\r\n\x05vMaxY\x18\x05 \x01(\r\"\xd9\x01\n\nMapExtInfo\x12\x15\n\rtaskBeginDate\x18\x01 \x01(\r\x12\x15\n\rmapUploadDate\x18\x02 \x01(\r\x12\x10\n\x08mapValid\x18\x03 \x01(\r\x12\x0e\n\x06radian\x18\x04 \x01(\r\x12\r\n\x05\x66orce\x18\x05 \x01(\r\x12\x11\n\tcleanPath\x18\x06 \x01(\r\x12/\n\x0b\x62oudaryInfo\x18\x07 \x01(\x0b\x32\x1a.b01.scmap.MapBoundaryInfo\x12\x12\n\nmapVersion\x18\x08 \x01(\r\x12\x14\n\x0cmapValueType\x18\t \x01(\r\"\x8a\x01\n\x0bMapHeadInfo\x12\x11\n\tmapHeadId\x18\x01 \x01(\r\x12\r\n\x05sizeX\x18\x02 \x01(\r\x12\r\n\x05sizeY\x18\x03 \x01(\r\x12\x0c\n\x04minX\x18\x04 \x01(\x02\x12\x0c\n\x04minY\x18\x05 \x01(\x02\x12\x0c\n\x04maxX\x18\x06 \x01(\x02\x12\x0c\n\x04maxY\x18\x07 \x01(\x02\x12\x12\n\nresolution\x18\x08 \x01(\x02\"\x1e\n\x0bMapDataInfo\x12\x0f\n\x07mapData\x18\x01 \x01(\x0c\"\x87\x02\n\x0cRoomDataInfo\x12\x0e\n\x06roomId\x18\x01 \x01(\r\x12\x10\n\x08roomName\x18\x02 \x01(\t\x12\x12\n\nroomTypeId\x18\x03 \x01(\r\x12\x12\n\nmeterialId\x18\x04 \x01(\r\x12\x12\n\ncleanState\x18\x05 \x01(\r\x12\x11\n\troomClean\x18\x06 \x01(\r\x12\x16\n\x0eroomCleanIndex\x18\x07 \x01(\r\x12\x30\n\x0croomNamePost\x18\x08 \x01(\x0b\x32\x1a.b01.scmap.DevicePointInfo\x12\x0f\n\x07\x63olorId\x18\n \x01(\r\x12\x17\n\x0f\x66loor_direction\x18\x0b \x01(\r\x12\x12\n\nglobal_seq\x18\x0c \x01(\r\"\xc7\x01\n\x08RobotMap\x12\x0f\n\x07mapType\x18\x01 \x01(\r\x12)\n\nmapExtInfo\x18\x02 \x01(\x0b\x32\x15.b01.scmap.MapExtInfo\x12\'\n\x07mapHead\x18\x03 \x01(\x0b\x32\x16.b01.scmap.MapHeadInfo\x12\'\n\x07mapData\x18\x04 \x01(\x0b\x32\x16.b01.scmap.MapDataInfo\x12-\n\x0croomDataInfo\x18\x0c \x03(\x0b\x32\x17.b01.scmap.RoomDataInfo')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'roborock.map.proto.b01_scmap_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ DESCRIPTOR._loaded_options = None
34
+ _globals['_DEVICEPOINTINFO']._serialized_start=49
35
+ _globals['_DEVICEPOINTINFO']._serialized_end=88
36
+ _globals['_MAPBOUNDARYINFO']._serialized_start=90
37
+ _globals['_MAPBOUNDARYINFO']._serialized_end=183
38
+ _globals['_MAPEXTINFO']._serialized_start=186
39
+ _globals['_MAPEXTINFO']._serialized_end=403
40
+ _globals['_MAPHEADINFO']._serialized_start=406
41
+ _globals['_MAPHEADINFO']._serialized_end=544
42
+ _globals['_MAPDATAINFO']._serialized_start=546
43
+ _globals['_MAPDATAINFO']._serialized_end=576
44
+ _globals['_ROOMDATAINFO']._serialized_start=579
45
+ _globals['_ROOMDATAINFO']._serialized_end=842
46
+ _globals['_ROBOTMAP']._serialized_start=845
47
+ _globals['_ROBOTMAP']._serialized_end=1044
48
+ # @@protoc_insertion_point(module_scope)
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import binascii
4
2
  import gzip
5
3
  import hashlib
@@ -1,7 +1,6 @@
1
- from __future__ import annotations
2
-
3
1
  from dataclasses import dataclass, field
4
2
  from enum import StrEnum
3
+ from typing import Self
5
4
 
6
5
  from roborock import RoborockEnum
7
6
  from roborock.util import get_next_int, get_timestamp
@@ -37,8 +36,8 @@ class RoborockDataProtocol(RoborockEnum):
37
36
  OFFLINE_STATUS = 135
38
37
 
39
38
  @classmethod
40
- def _missing_(cls: type[RoborockEnum], key) -> RoborockEnum:
41
- raise ValueError("%s not a valid key for Data Protocol", key)
39
+ def _missing_(cls: type[Self], key) -> Self:
40
+ raise ValueError(f"{key} not a valid key for Data Protocol")
42
41
 
43
42
 
44
43
  class RoborockDyadDataProtocol(RoborockEnum):
@@ -1,7 +1,6 @@
1
- from __future__ import annotations
2
-
3
1
  from dataclasses import dataclass, field
4
2
  from enum import Enum, StrEnum
3
+ from typing import Self
5
4
 
6
5
  from .data import (
7
6
  CleanRecord,
@@ -368,7 +367,7 @@ class DeviceProp(RoborockBase):
368
367
  ):
369
368
  self.dust_collection_mode_name = self.dock_summary.dust_collection_mode.mode.name
370
369
 
371
- def update(self, device_prop: DeviceProp) -> None:
370
+ def update(self, device_prop: Self) -> None:
372
371
  if device_prop.status:
373
372
  self.status = device_prop.status
374
373
  if device_prop.clean_summary:
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import logging
4
2
  import math
5
3
  import time
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import base64
4
2
  import hashlib
5
3
  import hmac