python-roborock 7.12.2__py3-none-any.whl → 7.12.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-roborock
3
- Version: 7.12.2
3
+ Version: 7.12.4
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/
@@ -28,9 +28,9 @@ roborock/data/mower/__init__.py,sha256=RXFcOE0-rchWViq3F-cLXB9Jnx4nKrn98TUR917sz
28
28
  roborock/data/mower/mower_code_mappings.py,sha256=T3NJEjgkYv3svIt7-gqFgrnNmVeBOkqit8_D52vSIao,1741
29
29
  roborock/data/mower/mower_containers.py,sha256=PYNVhbGCafkchenBlBmG_fUnIC-9WfxF8gNB0C4_9Yo,4106
30
30
  roborock/data/v1/__init__.py,sha256=Lx_D6baZrCMxp57D0NcdiA8rssJ5NnInZwUwrT79XOI,91
31
- roborock/data/v1/v1_clean_modes.py,sha256=RNi3_a1ZzDuaKpbEzFp-V1ugeJfIkjE4QdBI3YvI2G8,14886
31
+ roborock/data/v1/v1_clean_modes.py,sha256=lD0RnDa9hciRNi0VuWENjxbxflocdS1gCnK4fFrNoWY,15646
32
32
  roborock/data/v1/v1_code_mappings.py,sha256=hToQbd38mM3Dw8ONLyuDa2scqmNs0gNgOyqVgm-Gg7Y,12948
33
- roborock/data/v1/v1_containers.py,sha256=6erBnuQicFsdAsUaWIEAghzOxUU-xGuwqJBhcy_snZ4,17939
33
+ roborock/data/v1/v1_containers.py,sha256=s5ivZjkNIX7ls6IXBnppBWATTqdAlRY-Busw3aoKPnc,18602
34
34
  roborock/data/zeo/__init__.py,sha256=VxG5qG4M24DGOq2YAaxownWp6bEfj9DEOgOMHmTpcqY,63
35
35
  roborock/data/zeo/zeo_code_mappings.py,sha256=mfm2xoI8RllPZfyZE0HmUhUhLECwRD0llkMNWfaEHvw,8057
36
36
  roborock/data/zeo/zeo_containers.py,sha256=huDeisutagKz1rzsLZr_tGAietec_qOd0KB3i09YGgo,4339
@@ -92,7 +92,7 @@ roborock/devices/traits/v1/obstacle_photos.py,sha256=Aul1sOxJ6AxLWqI5AbeHHSzxfUP
92
92
  roborock/devices/traits/v1/rooms.py,sha256=SEAs3sHZxDmiiSWc9Korm838zu8ub_04CsmJVlT4F48,6237
93
93
  roborock/devices/traits/v1/routines.py,sha256=BfktlpnyDjD2DB3zBDS5BnmjZPE38aHa-wjMontPnik,931
94
94
  roborock/devices/traits/v1/smart_wash_params.py,sha256=6K3XrF5SRS39BWaMjjtGJgKX3lZ24jPsb6Nia4a9GVM,634
95
- roborock/devices/traits/v1/status.py,sha256=xmnu6pXXMYagihSBSJVSSR6pUSdgRaa1eAM2N1huIfU,6056
95
+ roborock/devices/traits/v1/status.py,sha256=P85-41-aY-_IYZaZjnDvY8OYgQx2NDHncvhKlIID_4E,6675
96
96
  roborock/devices/traits/v1/valley_electricity_timer.py,sha256=vE9bLJe8TdFGVh_9mDRkEoC3_DTBkDcdlSMSiYq0YTw,1863
97
97
  roborock/devices/traits/v1/volume.py,sha256=etRIYE0ynZZLEo2ez7Dm7_JbijmElrdjpn59hyLWqsQ,814
98
98
  roborock/devices/traits/v1/wash_towel_mode.py,sha256=bSXtaJ0ObDlXsJYuCHWCN3J0GyHEibPsDvOqBCxQW3Q,2039
@@ -103,11 +103,11 @@ roborock/devices/transport/mqtt_channel.py,sha256=dpL1mZIoI4Glk3hfUO9JweWFHG1tZW
103
103
  roborock/map/__init__.py,sha256=L3WHujuxUHCTTPaOElkJsTN_FkYhw01drfR-W4blgFc,146
104
104
  roborock/map/b01_grid_layers.py,sha256=b87MsGXslqLR_5H_4z4aj1wQqYgeTuULWqt4_txOF-M,12303
105
105
  roborock/map/b01_map_parser.py,sha256=y4Qlgiwre3bn5_UpL6R-ytjg8kQqwkbvhIYFOtpAM3s,14814
106
- roborock/map/b01_q10_map_parser.py,sha256=QWUu126wI2IzzFPrr7qrxcfnAgxU0LD0WRbSzhJB7LY,36715
106
+ roborock/map/b01_q10_map_parser.py,sha256=3DawtwPqyKhLUAW5Y4D6Ebm16l6RdWYiti0McELJCYQ,37066
107
107
  roborock/map/b01_q10_overlays.py,sha256=JmriUbT4-oe6NiE94VttMplFc1HHQrAGNQIiZHj5QKk,6640
108
- roborock/map/b01_q10_render.py,sha256=VmDxYbLHIARRRf8-Bg_05YxTyTElKN1y9AduEY0LQEY,13612
108
+ roborock/map/b01_q10_render.py,sha256=PFAYqFxRANCH141drkHnB_0hNGsH5RR-jtkYIo7b46Y,13626
109
109
  roborock/map/map_parser.py,sha256=EcJcz9yld_wu7d5QrmxR2BTfJ801F6SaEgJkbIIap4w,5111
110
- roborock/map/room_colors.py,sha256=5Q85oR_7xsUyzHYGOJPuzsZ6LQSCpJwfxfWql1v6T1E,2152
110
+ roborock/map/room_colors.py,sha256=YvjuqsUPPAH5exJ16V72OTikDT-KEjgF7vd91l78Pjs,2627
111
111
  roborock/map/proto/__init__.py,sha256=XcH_s-E67_hcQNwqSkdtY7DgYE0mRBBJwctzsf8zng4,60
112
112
  roborock/map/proto/b01_scmap.proto,sha256=lQtOruroUfrDK9EJ99cnGTi7avEK3CQ1haLIopDrS_A,3725
113
113
  roborock/map/proto/b01_scmap_pb2.py,sha256=HI7xxxpvV0cFl80B7T7RN74C3cP8YNqfukL_FW324ss,7493
@@ -127,8 +127,8 @@ roborock/testing/channel.py,sha256=vjulT5MHBe8lAq6XITUqcxJVcCILWx7ZEcw5imTx0Tk,7
127
127
  roborock/testing/cloud.py,sha256=L2L0KlZDYaMuoV19KUQUoq5EAUDeNQ1lJ8Ew5YaOgjo,11946
128
128
  roborock/testing/simulator.py,sha256=g8ZUWVSjgAzuRRxWOCeOq-7deodyEItwUpqEw9RzCmQ,4347
129
129
  roborock/testing/v1_simulator.py,sha256=_LX_vsBMcB2Uu2QDt8QnQymgtVtbnwD5Xa7tmhiPAnY,14439
130
- python_roborock-7.12.2.dist-info/METADATA,sha256=ZULsv1uHLLGPW-ZYGcBDhHIlluaM_fwPfqrIDkrdfzc,5279
131
- python_roborock-7.12.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
132
- python_roborock-7.12.2.dist-info/entry_points.txt,sha256=EvC1nMqi9ZXKgZnqlNXA33v_3nzgPNjnM0mzWlrehnY,47
133
- python_roborock-7.12.2.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
134
- python_roborock-7.12.2.dist-info/RECORD,,
130
+ python_roborock-7.12.4.dist-info/METADATA,sha256=fuYSHig-rrR-C1NJMjLKBQ-D6gjsY3GfdAWT0KTgZVU,5279
131
+ python_roborock-7.12.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
132
+ python_roborock-7.12.4.dist-info/entry_points.txt,sha256=EvC1nMqi9ZXKgZnqlNXA33v_3nzgPNjnM0mzWlrehnY,47
133
+ python_roborock-7.12.4.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
134
+ python_roborock-7.12.4.dist-info/RECORD,,
@@ -199,14 +199,20 @@ def get_water_modes(features: DeviceFeatures) -> list[WaterModes]:
199
199
 
200
200
  def get_water_slide_mode_name(code: int) -> str | None:
201
201
  """Get the app's range name for a water slide code, or None if out of range."""
202
+ mode = _resolve_water_slide_mode(code)
203
+ return mode.value if mode is not None else None
204
+
205
+
206
+ def _resolve_water_slide_mode(code: int) -> WaterModes | None:
207
+ """Resolve a raw slider code to the water mode for its app range."""
202
208
  if code == 200:
203
- return WaterModes.OFF.value
209
+ return WaterModes.OFF
204
210
  position = code - 220
205
211
  if position < 1:
206
212
  return None
207
213
  for last_position, mode in _WATER_SLIDE_RANGES:
208
214
  if position <= last_position:
209
- return mode.value
215
+ return mode
210
216
  return None
211
217
 
212
218
 
@@ -300,7 +306,14 @@ def get_cleaning_mode_parameters(cleaning_mode: CleaningMode, features: DeviceFe
300
306
  raise RoborockUnsupportedFeature(f"Cleaning mode {cleaning_mode.value!r} is not supported")
301
307
 
302
308
  fan_power, water_box_mode, mop_mode = _get_clean_motor_mode_params(cleaning_mode, features)
303
- params: dict[str, int] = {"fan_power": fan_power.code, "water_box_mode": water_box_mode.code}
309
+ water_code = water_box_mode.code
310
+ if features.is_water_slide_mode_supported:
311
+ # Aliased water enums retain legacy codes; the device mapping preserves
312
+ # slide-specific wire values without changing the user-facing names.
313
+ water_code = next(
314
+ code for code, name in get_water_mode_mapping(features).items() if name == water_box_mode.value
315
+ )
316
+ params: dict[str, int] = {"fan_power": fan_power.code, "water_box_mode": water_code}
304
317
  if features.is_clean_route_setting_supported:
305
318
  params["mop_mode"] = mop_mode.code
306
319
  return [params]
@@ -337,6 +350,8 @@ def get_current_cleaning_mode(
337
350
  return None
338
351
  clean_mode_enum = _resolve_clean_mode(clean_mode, features)
339
352
  water_mode_enum = _resolve_mode_code(water_mode, WaterModes)
353
+ if features.is_water_slide_mode_supported and isinstance(water_mode, int):
354
+ water_mode_enum = _resolve_water_slide_mode(water_mode) or water_mode_enum
340
355
  mop_mode_enum = _resolve_mode_code(mop_mode, CleanRoutes)
341
356
  if clean_mode_enum is None or water_mode_enum is None:
342
357
  return None
@@ -1,5 +1,6 @@
1
1
  import datetime
2
2
  import logging
3
+ import warnings
3
4
  from dataclasses import dataclass, field
4
5
  from enum import StrEnum
5
6
  from typing import Any
@@ -73,6 +74,39 @@ class StatusField(FieldNameBase):
73
74
  RDT = "rdt"
74
75
 
75
76
 
77
+ def get_dock_state(
78
+ state: RoborockStateCode | int | None,
79
+ battery: int | None,
80
+ charge_status: RoborockChargeStatus | int | None = None,
81
+ *,
82
+ is_supported_valley_electricity: bool = False,
83
+ ) -> RoborockDockState:
84
+ """Return the dock state, accounting for off-peak charging support."""
85
+ if state is None or state == RoborockStateCode.unknown:
86
+ return RoborockDockState.unknown
87
+
88
+ # 6. DUSTING
89
+ if state == RoborockStateCode.emptying_the_bin:
90
+ return RoborockDockState.dusting
91
+
92
+ # 5. FULL
93
+ if state == RoborockStateCode.charging_complete or (state == RoborockStateCode.charging and battery == 100):
94
+ return RoborockDockState.full
95
+
96
+ # 3 & 4. CHARGING and CHARGE_WAITING
97
+ if state == RoborockStateCode.charging:
98
+ if is_supported_valley_electricity and charge_status == RoborockChargeStatus.charge_waiting:
99
+ return RoborockDockState.off_peak_waiting
100
+ return RoborockDockState.charging
101
+
102
+ # 2. RECHARGING
103
+ if state in (RoborockStateCode.returning_home, RoborockStateCode.docking):
104
+ return RoborockDockState.returning
105
+
106
+ # 1. IDLE (Not on dock, or doing something else)
107
+ return RoborockDockState.idle
108
+
109
+
76
110
  @dataclass
77
111
  class StatusV2(RoborockBase):
78
112
  """The result of a GET_STATUS API request."""
@@ -219,36 +253,26 @@ class StatusV2(RoborockBase):
219
253
  def dock_state(self) -> RoborockDockState:
220
254
  """A synthesized, high-level dock state reflecting the UI's display.
221
255
 
222
- This property simplifies integration by handling the complex logic
223
- of checking state, charge_status, and battery level simultaneously. It handles
224
- newer off-peak charging logic seamlessly while maintaining backwards compatibility
225
- with older devices.
256
+ .. deprecated::
257
+ Use StatusTrait.dock_state or get_dock_state instead.
226
258
  """
227
- if self.state is None or self.state == RoborockStateCode.unknown:
228
- return RoborockDockState.unknown
229
-
230
- # 6. DUSTING
231
- if self.state == RoborockStateCode.emptying_the_bin:
232
- return RoborockDockState.dusting
233
-
234
- # 5. FULL
235
- if self.state == RoborockStateCode.charging_complete or (
236
- self.state == RoborockStateCode.charging and self.battery == 100
237
- ):
238
- return RoborockDockState.full
239
-
240
- # 3 & 4. CHARGING and CHARGE_WAITING
241
- if self.state == RoborockStateCode.charging:
242
- if self.charge_status == RoborockChargeStatus.charge_waiting:
243
- return RoborockDockState.off_peak_waiting
244
- return RoborockDockState.charging
245
-
246
- # 2. RECHARGING
247
- if self.state in (RoborockStateCode.returning_home, RoborockStateCode.docking):
248
- return RoborockDockState.returning
249
-
250
- # 1. IDLE (Not on dock, or doing something else)
251
- return RoborockDockState.idle
259
+ warnings.warn(
260
+ "StatusV2.dock_state is deprecated and will be removed in a future release. "
261
+ "Use StatusTrait.dock_state or get_dock_state instead.",
262
+ DeprecationWarning,
263
+ stacklevel=2,
264
+ )
265
+ return get_dock_state(
266
+ state=self.state,
267
+ battery=self.battery,
268
+ charge_status=self.charge_status,
269
+ is_supported_valley_electricity=True,
270
+ )
271
+
272
+ @property
273
+ def is_battery_charging(self) -> bool:
274
+ """Return whether the battery is actively charging."""
275
+ return self.dock_state == RoborockDockState.charging
252
276
 
253
277
  def __repr__(self) -> str:
254
278
  return _attr_repr(self)
@@ -5,6 +5,7 @@ from typing import Any
5
5
  from roborock import (
6
6
  CleaningMode,
7
7
  CleanRoutes,
8
+ RoborockDockState,
8
9
  StatusV2,
9
10
  VacuumModes,
10
11
  WaterModes,
@@ -13,6 +14,7 @@ from roborock import (
13
14
  get_cleaning_mode_options,
14
15
  get_cleaning_mode_parameters,
15
16
  get_current_cleaning_mode,
17
+ get_dock_state,
16
18
  get_water_mode_mapping,
17
19
  get_water_modes,
18
20
  get_water_slide_mode_name,
@@ -66,6 +68,21 @@ class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener):
66
68
  self._device_features_trait = device_feature_trait
67
69
  self._region = region
68
70
 
71
+ @property
72
+ def dock_state(self) -> RoborockDockState:
73
+ """Return the feature-aware dock state used by the Roborock app."""
74
+ return get_dock_state(
75
+ state=self.state,
76
+ battery=self.battery,
77
+ charge_status=self.charge_status,
78
+ is_supported_valley_electricity=self._device_features_trait.is_supported_valley_electricity,
79
+ )
80
+
81
+ @property
82
+ def is_battery_charging(self) -> bool:
83
+ """Return whether the battery is actively charging."""
84
+ return self.dock_state == RoborockDockState.charging
85
+
69
86
  @cached_property
70
87
  def fan_speed_options(self) -> list[VacuumModes]:
71
88
  return get_clean_modes(self._device_features_trait)
@@ -801,8 +801,8 @@ class B01Q10MapParser:
801
801
  """
802
802
  return self.parsed_from_packet(packet)
803
803
 
804
- def parsed_from_packet(self, packet: Q10MapPacket) -> ParsedMapData:
805
- """Render a (possibly erase-modified) packet into a PNG + ``MapData``."""
804
+ def map_data_from_packet(self, packet: Q10MapPacket) -> MapData:
805
+ """Build MapData with a rendered base image, without PNG serialization."""
806
806
  image = self._render(packet)
807
807
 
808
808
  map_data = MapData()
@@ -830,17 +830,23 @@ class B01Q10MapParser:
830
830
  if packet.carpet_mask is not None:
831
831
  map_data.carpet_map = {i for i, value in enumerate(packet.carpet_mask) if value}
832
832
 
833
+ return map_data
834
+
835
+ def parsed_from_packet(self, packet: Q10MapPacket) -> ParsedMapData:
836
+ """Render a (possibly erase-modified) packet into a PNG + ``MapData``."""
837
+ map_data = self.map_data_from_packet(packet)
833
838
  image_bytes = io.BytesIO()
834
- image.save(image_bytes, format=_MAP_FILE_FORMAT)
839
+ if map_data.image is not None:
840
+ map_data.image.data.save(image_bytes, format=_MAP_FILE_FORMAT)
835
841
  return ParsedMapData(image_content=image_bytes.getvalue(), map_data=map_data)
836
842
 
837
843
  def _render(self, packet: Q10MapPacket) -> Image.Image:
838
844
  """Render the Q10 grid with the V1 map palette."""
839
845
  palette = _build_palette(packet.grid, packet.width)
840
- rgba = bytearray()
841
- for value in packet.grid:
842
- rgba.extend(palette[value])
843
- img = Image.frombytes("RGBA", (packet.width, packet.height), bytes(rgba))
846
+ flat_palette = [channel for color in palette for channel in color]
847
+ p_img = Image.frombytes("P", (packet.width, packet.height), packet.grid)
848
+ p_img.putpalette(flat_palette, rawmode="RGBA")
849
+ img = p_img.convert("RGBA")
844
850
  # The ss07 grid is stored top-down (row 0 = top of the home), so it is
845
851
  # rendered as-is -- unlike the V1/Q7 convention, no vertical flip.
846
852
  scale = self._config.map_scale
@@ -109,10 +109,9 @@ def render_q10_map(
109
109
  # areas disappear (as the app shows).
110
110
  render_packet = erased_packet(packet, cells)
111
111
 
112
- parsed = parser.parsed_from_packet(render_packet)
113
- if parsed.image_content is None or parsed.map_data is None:
112
+ map_data = parser.map_data_from_packet(render_packet)
113
+ if map_data.image is None:
114
114
  raise RoborockException("Failed to render Q10 map image")
115
- map_data = parsed.map_data
116
115
 
117
116
  has_drawables = False
118
117
  if trace_calibration is not None and trace is not None:
@@ -128,7 +127,9 @@ def render_q10_map(
128
127
  if has_drawables:
129
128
  return _draw_map_content(map_data, config=config)
130
129
 
131
- return parsed.image_content
130
+ buffer = io.BytesIO()
131
+ map_data.image.data.save(buffer, format="PNG")
132
+ return buffer.getvalue()
132
133
 
133
134
 
134
135
  def solve_q10_calibration(
@@ -23,23 +23,31 @@ def adjacency_aware_room_colors(
23
23
  if width <= 0:
24
24
  return {}
25
25
 
26
+ lut = [room_id_from_cell(i) for i in range(256)]
26
27
  room_ids: set[int] = set()
27
28
  neighbors: dict[int, set[int]] = {}
28
29
  for index, value in enumerate(grid):
29
- room_id = room_id_from_cell(value)
30
+ room_id = lut[value] if 0 <= value < 256 else room_id_from_cell(value)
30
31
  if room_id is None:
31
32
  continue
32
33
  room_ids.add(room_id)
33
34
  neighbors.setdefault(room_id, set())
34
35
 
35
- for neighbor_index in (index - 1 if index % width else -1, index - width):
36
- if neighbor_index < 0:
37
- continue
38
- neighbor_id = room_id_from_cell(grid[neighbor_index])
39
- if neighbor_id is None or neighbor_id == room_id:
40
- continue
41
- neighbors[room_id].add(neighbor_id)
42
- neighbors.setdefault(neighbor_id, set()).add(room_id)
36
+ if index % width:
37
+ left_val = grid[index - 1]
38
+ if left_val != value:
39
+ neighbor_id = lut[left_val] if 0 <= left_val < 256 else room_id_from_cell(left_val)
40
+ if neighbor_id is not None and neighbor_id != room_id:
41
+ neighbors[room_id].add(neighbor_id)
42
+ neighbors.setdefault(neighbor_id, set()).add(room_id)
43
+
44
+ if index >= width:
45
+ up_val = grid[index - width]
46
+ if up_val != value:
47
+ neighbor_id = lut[up_val] if 0 <= up_val < 256 else room_id_from_cell(up_val)
48
+ if neighbor_id is not None and neighbor_id != room_id:
49
+ neighbors[room_id].add(neighbor_id)
50
+ neighbors.setdefault(neighbor_id, set()).add(room_id)
43
51
 
44
52
  candidates: list[Color] = []
45
53
  for palette_id in map(int, ColorsPalette.ROOM_COLORS):