roborock.data.b01_q10.b01_q10_containers

Data container classes for Q10 B01 devices.

Many of these classes use the field(metadata={"dps": ...}) convention to map dataclass fields to device Data Points (DPS). This metadata is utilized by the UpdatableTrait helper in roborock.devices.traits.b01.q10.common to automatically update objects from raw device responses.

  1"""Data container classes for Q10 B01 devices.
  2
  3Many of these classes use the `field(metadata={"dps": ...})` convention to map
  4dataclass fields to device Data Points (DPS). This metadata is utilized by the
  5`UpdatableTrait` helper in `roborock.devices.traits.b01.q10.common` to
  6automatically update objects from raw device responses.
  7"""
  8
  9import datetime
 10from dataclasses import dataclass, field
 11
 12from ..containers import RoborockBase
 13from .b01_q10_code_mappings import (
 14    B01_Q10_DP,
 15    YXAreaUnit,
 16    YXBackType,
 17    YXCarpetCleanType,
 18    YXCleaningResult,
 19    YXCleanLine,
 20    YXCleanScope,
 21    YXCleanType,
 22    YXDeviceCleanTask,
 23    YXDeviceDustCollectionFrequency,
 24    YXDeviceState,
 25    YXFanLevel,
 26    YXFault,
 27    YXStartMethod,
 28    YXWaterLevel,
 29)
 30
 31_ROBOROCK_COORDINATE_OFFSET_MM = 25_500
 32_Q10_TRACE_UNIT_MM = 2.5
 33_Q10_VECTOR_UNIT_MM = 5
 34
 35
 36@dataclass(frozen=True)
 37class Q10RoborockPoint:
 38    """A point in the common Roborock millimetre coordinate space.
 39
 40    Q10 trace and vector coordinates are firmware details. Public Q10 APIs use
 41    this coordinate system, matching other Roborock devices and placing the dock
 42    at ``(25500, 25500)``.
 43    """
 44
 45    x: int
 46    y: int
 47
 48    @classmethod
 49    def from_trace(cls, x: int, y: int) -> "Q10RoborockPoint":
 50        """Convert Q10 trace coordinates to common Roborock coordinates."""
 51        for value in (x, y):
 52            if isinstance(value, bool) or not isinstance(value, int):
 53                raise ValueError("trace coordinates must be integers")
 54        return cls(
 55            x=round(_ROBOROCK_COORDINATE_OFFSET_MM + x * _Q10_TRACE_UNIT_MM),
 56            y=round(_ROBOROCK_COORDINATE_OFFSET_MM + y * _Q10_TRACE_UNIT_MM),
 57        )
 58
 59    @classmethod
 60    def from_vector(cls, x: int, y: int) -> "Q10RoborockPoint":
 61        """Convert Q10 vector coordinates to common Roborock coordinates."""
 62        for value in (x, y):
 63            if isinstance(value, bool) or not isinstance(value, int):
 64                raise ValueError("vector coordinates must be integers")
 65            if not -(2**15) <= value < 2**15:
 66                raise ValueError("vector coordinates are outside the Q10 map range")
 67        return cls(
 68            x=_ROBOROCK_COORDINATE_OFFSET_MM + x * _Q10_VECTOR_UNIT_MM,
 69            y=_ROBOROCK_COORDINATE_OFFSET_MM + y * _Q10_VECTOR_UNIT_MM,
 70        )
 71
 72    def to_vector(self) -> tuple[int, int]:
 73        """Convert common Roborock coordinates to the Q10 vector grid."""
 74        coordinates: list[int] = []
 75        for value in (self.x, self.y):
 76            if isinstance(value, bool) or not isinstance(value, int):
 77                raise ValueError("coordinates must be integers")
 78            relative_mm = value - _ROBOROCK_COORDINATE_OFFSET_MM
 79            if relative_mm % _Q10_VECTOR_UNIT_MM:
 80                raise ValueError("coordinates must align to the Q10 5 mm grid")
 81            coordinate = relative_mm // _Q10_VECTOR_UNIT_MM
 82            if not -(2**15) <= coordinate < 2**15:
 83                raise ValueError("coordinates are outside the Q10 map range")
 84            coordinates.append(coordinate)
 85        return coordinates[0], coordinates[1]
 86
 87
 88@dataclass
 89class dpCleanRecord(RoborockBase):
 90    op: str
 91    result: int
 92    id: str
 93    data: list
 94
 95
 96@dataclass
 97class Q10CleanRecord(RoborockBase):
 98    """A single Q10 (ss07) clean record decoded from a ``dpCleanRecord`` (DP 52) entry.
 99
100    The device returns each record as a 12-field underscore-delimited string in the
101    ``data`` list of a ``{"op": "list"}`` query (or the ``id`` of an ``{"op": "notify"}``
102    push). The ``*_len`` values are internal blob-length metrics whose units aren't
103    confirmed; the original ``raw`` string is always retained. The enum fields resolve
104    an unmapped/unset code to ``None`` rather than guessing.
105    """
106
107    raw: str
108    record_id: str | None = None
109    start_time: int | None = None
110    """Clean start time, Unix seconds."""
111    clean_time: int | None = None
112    """Cleaning time, minutes."""
113    clean_area: int | None = None
114    """Cleaned area in square meters."""
115    map_len: int | None = None
116    """Length of the saved map blob for this record (0 = none stored)."""
117    path_len: int | None = None
118    """Length of the saved path blob for this record (0 = none stored)."""
119    virtual_len: int | None = None
120    """Length of the saved virtual-restriction blob for this record (0 = none stored)."""
121    clean_mode: YXCleanScope | None = None
122    """Clean scope/type (full / selective-room / zone / spot). Same axis as the live
123    :class:`YXDeviceCleanTask` but a different record encoding -- see :class:`YXCleanScope`."""
124    work_mode: YXCleanType | None = None
125    """Actual work performed (vac+mop / vacuum / mop) -- the same enum :class:`Q10Status`
126    uses for the live clean-mode DP. Records only ever carry 1/2/3 here."""
127    cleaning_result: YXCleaningResult | None = None
128    """How the clean ended: 0 interrupted (fault), 1 completed, 2 stopped (no fault)."""
129    start_method: YXStartMethod | None = None
130    """What initiated the clean: 0 remote, 1 app, 2 timer, 3 button."""
131    collect_dust_count: int | None = None
132    """Number of dock auto-empties during the clean."""
133
134    @property
135    def start_datetime(self) -> datetime.datetime | None:
136        """The start time as a timezone-aware (UTC) datetime."""
137        if self.start_time is not None:
138            return datetime.datetime.fromtimestamp(self.start_time).astimezone(datetime.UTC)
139        return None
140
141
142@dataclass
143class Q10MapInfo(RoborockBase):
144    """A saved map reported by ``dpMultiMap``.
145
146    Q10 firmware represents the map identifier as a string on the wire. The
147    value is sent back unchanged in a subsequent ``{"op": "select"}`` detail
148    request. On Q10 firmware, ``select`` previews a saved map without applying
149    it as the active map.
150    """
151
152    id: str
153    name: str | None = None
154    timestamp: int | None = None
155
156
157@dataclass
158class dpMultiMap(RoborockBase):
159    """Response envelope for the Q10 ``dpMultiMap`` data point."""
160
161    op: str
162    result: int
163    data: list[Q10MapInfo] = field(default_factory=list)
164
165    @property
166    def current_map_id(self) -> str | None:
167        """Return the first saved-map identifier, if one was reported."""
168        first = next((map_info for map_info in self.data if map_info.id), None)
169        return first.id if first else None
170
171
172@dataclass
173class dpGetCarpet(RoborockBase):
174    op: str
175    result: int
176    data: str
177
178
179@dataclass
180class dpSelfIdentifyingCarpet(RoborockBase):
181    op: str
182    result: int
183    data: str
184
185
186@dataclass
187class dpNetInfo(RoborockBase):
188    wifi_name: str | None = None
189    # "ip_adress" intentionally mirrors the device's "ipAdress" key (sic).
190    ip_adress: str | None = None
191    mac: str | None = None
192    signal: int | None = None
193
194    @property
195    def ip_address(self) -> str | None:
196        """Correctly-spelled alias for :attr:`ip_adress`."""
197        return self.ip_adress
198
199
200@dataclass
201class dpNotDisturbExpand(RoborockBase):
202    disturb_dust_enable: int | None = None
203    disturb_light: int | None = None
204    disturb_resume_clean: int | None = None
205    disturb_voice: int | None = None
206
207
208@dataclass
209class dpCurrentCleanRoomIds(RoborockBase):
210    room_id_list: list
211
212
213@dataclass
214class dpVoiceVersion(RoborockBase):
215    version: int
216
217
218@dataclass
219class dpTimeZone(RoborockBase):
220    time_zone_city: str | None = None
221    time_zone_sec: int | None = None
222
223
224@dataclass
225class Q10Status(RoborockBase):
226    """Core vacuum status for Q10 devices.
227
228    Fields are mapped to DPS values using metadata. Objects of this class can be
229    automatically updated using the `UpdatableTrait` helper. Settings that have
230    their own trait (volume, child lock, do-not-disturb, dust collection,
231    network info, consumables) live on those traits instead of here.
232    """
233
234    clean_time: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TIME})
235    clean_area: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_AREA})
236    battery: int | None = field(default=None, metadata={"dps": B01_Q10_DP.BATTERY})
237    status: YXDeviceState | None = field(default=None, metadata={"dps": B01_Q10_DP.STATUS})
238    fan_level: YXFanLevel | None = field(default=None, metadata={"dps": B01_Q10_DP.FAN_LEVEL})
239    water_level: YXWaterLevel | None = field(default=None, metadata={"dps": B01_Q10_DP.WATER_LEVEL})
240    clean_count: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_COUNT})
241    total_clean_area: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_AREA})
242    total_clean_count: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_COUNT})
243    total_clean_time: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_TIME})
244    clean_mode: YXCleanType | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_MODE})
245    clean_task_type: YXDeviceCleanTask | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TASK_TYPE})
246    back_type: YXBackType | None = field(default=None, metadata={"dps": B01_Q10_DP.BACK_TYPE})
247    cleaning_progress: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_PROGRESS})
248    fault: YXFault | None = field(default=None, metadata={"dps": B01_Q10_DP.FAULT})
249
250    # Additional state reported in the device's full status dump.
251    clean_line: YXCleanLine | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_LINE})
252    carpet_clean_type: YXCarpetCleanType | None = field(default=None, metadata={"dps": B01_Q10_DP.CARPET_CLEAN_TYPE})
253    area_unit: YXAreaUnit | None = field(default=None, metadata={"dps": B01_Q10_DP.AREA_UNIT})
254    auto_boost: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.AUTO_BOOST})
255    multi_map_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MULTI_MAP_SWITCH})
256    map_save_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MAP_SAVE_SWITCH})
257    recent_clean_record: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.RECENT_CLEAN_RECORD})
258    valley_point_charging: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.VALLEY_POINT_CHARGING})
259    line_laser_obstacle_avoidance: bool | None = field(
260        default=None, metadata={"dps": B01_Q10_DP.LINE_LASER_OBSTACLE_AVOIDANCE}
261    )
262    # Whether a mop module is attached, and whether "clean along floor direction" is on.
263    mop_state: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MOP_STATE})
264    ground_clean: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.GROUND_CLEAN})
265    # True while an "add area" / re-clean (the app's draw-a-rectangle "re cleaning")
266    # request is in progress; pulses back to False once the robot has the area.
267    add_clean_state: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.ADD_CLEAN_STATE})
268    robot_country_code: str | None = field(default=None, metadata={"dps": B01_Q10_DP.ROBOT_COUNTRY_CODE})
269    time_zone: dpTimeZone | None = field(default=None, metadata={"dps": B01_Q10_DP.TIME_ZONE})
270
271    # TODO(#846): value mappings for these ints are not yet decoded (no app
272    # control found / internal / constant); keep as int until reverse-engineered.
273    breakpoint_clean: int | None = field(default=None, metadata={"dps": B01_Q10_DP.BREAKPOINT_CLEAN})
274    timer_type: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TIMER_TYPE})
275    user_plan: int | None = field(default=None, metadata={"dps": B01_Q10_DP.USER_PLAN})
276    robot_type: int | None = field(default=None, metadata={"dps": B01_Q10_DP.ROBOT_TYPE})
277
278    # DEPRECATED: consumable/accessory remaining-life now lives on the
279    # ``Q10Consumable`` trait. These aliases are kept here for backwards
280    # compatibility and will be removed in a follow-up release. See PR #846.
281    main_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.MAIN_BRUSH_LIFE})
282    side_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SIDE_BRUSH_LIFE})
283    filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
284    sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
285
286    @property
287    def fault_name(self) -> str | None:
288        """Returns the name of the current fault."""
289        return self.fault.value if self.fault is not None else None
290
291
292@dataclass
293class SoundVolume(RoborockBase):
294    """Speaker volume read-model (0-100)."""
295
296    volume: int | None = field(default=None, metadata={"dps": B01_Q10_DP.VOLUME})
297
298
299@dataclass
300class ChildLock(RoborockBase):
301    """Child-lock read-model."""
302
303    child_lock: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.CHILD_LOCK})
304
305
306@dataclass
307class DoNotDisturb(RoborockBase):
308    """Do Not Disturb read-model."""
309
310    not_disturb: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.NOT_DISTURB})
311    not_disturb_expand: dpNotDisturbExpand | None = field(default=None, metadata={"dps": B01_Q10_DP.NOT_DISTURB_EXPAND})
312
313
314@dataclass
315class DustCollection(RoborockBase):
316    """Dock auto-empty (dust collection) read-model."""
317
318    dust_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.DUST_SWITCH})
319    dust_setting: YXDeviceDustCollectionFrequency | None = field(
320        default=None, metadata={"dps": B01_Q10_DP.DUST_SETTING}
321    )
322
323
324@dataclass
325class Q10Consumable(RoborockBase):
326    """Consumable / accessory remaining-life read-model.
327
328    Named with a ``Q10`` prefix to avoid shadowing the v1 ``Consumable`` when both
329    are star-imported into the ``roborock.data`` namespace.
330    """
331
332    main_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.MAIN_BRUSH_LIFE})
333    side_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SIDE_BRUSH_LIFE})
334    filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
335    sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
336
337
338@dataclass
339class Q10NetworkInfo(RoborockBase):
340    """Network information read-model.
341
342    Named with a ``Q10`` prefix to avoid shadowing the v1 ``NetworkInfo`` when both
343    are star-imported into the ``roborock.data`` namespace.
344    """
345
346    net_info: dpNetInfo | None = field(default=None, metadata={"dps": B01_Q10_DP.NET_INFO})
@dataclass(frozen=True)
class Q10RoborockPoint:
37@dataclass(frozen=True)
38class Q10RoborockPoint:
39    """A point in the common Roborock millimetre coordinate space.
40
41    Q10 trace and vector coordinates are firmware details. Public Q10 APIs use
42    this coordinate system, matching other Roborock devices and placing the dock
43    at ``(25500, 25500)``.
44    """
45
46    x: int
47    y: int
48
49    @classmethod
50    def from_trace(cls, x: int, y: int) -> "Q10RoborockPoint":
51        """Convert Q10 trace coordinates to common Roborock coordinates."""
52        for value in (x, y):
53            if isinstance(value, bool) or not isinstance(value, int):
54                raise ValueError("trace coordinates must be integers")
55        return cls(
56            x=round(_ROBOROCK_COORDINATE_OFFSET_MM + x * _Q10_TRACE_UNIT_MM),
57            y=round(_ROBOROCK_COORDINATE_OFFSET_MM + y * _Q10_TRACE_UNIT_MM),
58        )
59
60    @classmethod
61    def from_vector(cls, x: int, y: int) -> "Q10RoborockPoint":
62        """Convert Q10 vector coordinates to common Roborock coordinates."""
63        for value in (x, y):
64            if isinstance(value, bool) or not isinstance(value, int):
65                raise ValueError("vector coordinates must be integers")
66            if not -(2**15) <= value < 2**15:
67                raise ValueError("vector coordinates are outside the Q10 map range")
68        return cls(
69            x=_ROBOROCK_COORDINATE_OFFSET_MM + x * _Q10_VECTOR_UNIT_MM,
70            y=_ROBOROCK_COORDINATE_OFFSET_MM + y * _Q10_VECTOR_UNIT_MM,
71        )
72
73    def to_vector(self) -> tuple[int, int]:
74        """Convert common Roborock coordinates to the Q10 vector grid."""
75        coordinates: list[int] = []
76        for value in (self.x, self.y):
77            if isinstance(value, bool) or not isinstance(value, int):
78                raise ValueError("coordinates must be integers")
79            relative_mm = value - _ROBOROCK_COORDINATE_OFFSET_MM
80            if relative_mm % _Q10_VECTOR_UNIT_MM:
81                raise ValueError("coordinates must align to the Q10 5 mm grid")
82            coordinate = relative_mm // _Q10_VECTOR_UNIT_MM
83            if not -(2**15) <= coordinate < 2**15:
84                raise ValueError("coordinates are outside the Q10 map range")
85            coordinates.append(coordinate)
86        return coordinates[0], coordinates[1]

A point in the common Roborock millimetre coordinate space.

Q10 trace and vector coordinates are firmware details. Public Q10 APIs use this coordinate system, matching other Roborock devices and placing the dock at (25500, 25500).

Q10RoborockPoint(x: int, y: int)
x: int
y: int
@classmethod
def from_trace( cls, x: int, y: int) -> Q10RoborockPoint:
49    @classmethod
50    def from_trace(cls, x: int, y: int) -> "Q10RoborockPoint":
51        """Convert Q10 trace coordinates to common Roborock coordinates."""
52        for value in (x, y):
53            if isinstance(value, bool) or not isinstance(value, int):
54                raise ValueError("trace coordinates must be integers")
55        return cls(
56            x=round(_ROBOROCK_COORDINATE_OFFSET_MM + x * _Q10_TRACE_UNIT_MM),
57            y=round(_ROBOROCK_COORDINATE_OFFSET_MM + y * _Q10_TRACE_UNIT_MM),
58        )

Convert Q10 trace coordinates to common Roborock coordinates.

@classmethod
def from_vector( cls, x: int, y: int) -> Q10RoborockPoint:
60    @classmethod
61    def from_vector(cls, x: int, y: int) -> "Q10RoborockPoint":
62        """Convert Q10 vector coordinates to common Roborock coordinates."""
63        for value in (x, y):
64            if isinstance(value, bool) or not isinstance(value, int):
65                raise ValueError("vector coordinates must be integers")
66            if not -(2**15) <= value < 2**15:
67                raise ValueError("vector coordinates are outside the Q10 map range")
68        return cls(
69            x=_ROBOROCK_COORDINATE_OFFSET_MM + x * _Q10_VECTOR_UNIT_MM,
70            y=_ROBOROCK_COORDINATE_OFFSET_MM + y * _Q10_VECTOR_UNIT_MM,
71        )

Convert Q10 vector coordinates to common Roborock coordinates.

def to_vector(self) -> tuple[int, int]:
73    def to_vector(self) -> tuple[int, int]:
74        """Convert common Roborock coordinates to the Q10 vector grid."""
75        coordinates: list[int] = []
76        for value in (self.x, self.y):
77            if isinstance(value, bool) or not isinstance(value, int):
78                raise ValueError("coordinates must be integers")
79            relative_mm = value - _ROBOROCK_COORDINATE_OFFSET_MM
80            if relative_mm % _Q10_VECTOR_UNIT_MM:
81                raise ValueError("coordinates must align to the Q10 5 mm grid")
82            coordinate = relative_mm // _Q10_VECTOR_UNIT_MM
83            if not -(2**15) <= coordinate < 2**15:
84                raise ValueError("coordinates are outside the Q10 map range")
85            coordinates.append(coordinate)
86        return coordinates[0], coordinates[1]

Convert common Roborock coordinates to the Q10 vector grid.

@dataclass
class dpCleanRecord(roborock.data.containers.RoborockBase):
89@dataclass
90class dpCleanRecord(RoborockBase):
91    op: str
92    result: int
93    id: str
94    data: list
dpCleanRecord(op: str, result: int, id: str, data: list)
op: str
result: int
id: str
data: list
@dataclass
class Q10CleanRecord(roborock.data.containers.RoborockBase):
 97@dataclass
 98class Q10CleanRecord(RoborockBase):
 99    """A single Q10 (ss07) clean record decoded from a ``dpCleanRecord`` (DP 52) entry.
100
101    The device returns each record as a 12-field underscore-delimited string in the
102    ``data`` list of a ``{"op": "list"}`` query (or the ``id`` of an ``{"op": "notify"}``
103    push). The ``*_len`` values are internal blob-length metrics whose units aren't
104    confirmed; the original ``raw`` string is always retained. The enum fields resolve
105    an unmapped/unset code to ``None`` rather than guessing.
106    """
107
108    raw: str
109    record_id: str | None = None
110    start_time: int | None = None
111    """Clean start time, Unix seconds."""
112    clean_time: int | None = None
113    """Cleaning time, minutes."""
114    clean_area: int | None = None
115    """Cleaned area in square meters."""
116    map_len: int | None = None
117    """Length of the saved map blob for this record (0 = none stored)."""
118    path_len: int | None = None
119    """Length of the saved path blob for this record (0 = none stored)."""
120    virtual_len: int | None = None
121    """Length of the saved virtual-restriction blob for this record (0 = none stored)."""
122    clean_mode: YXCleanScope | None = None
123    """Clean scope/type (full / selective-room / zone / spot). Same axis as the live
124    :class:`YXDeviceCleanTask` but a different record encoding -- see :class:`YXCleanScope`."""
125    work_mode: YXCleanType | None = None
126    """Actual work performed (vac+mop / vacuum / mop) -- the same enum :class:`Q10Status`
127    uses for the live clean-mode DP. Records only ever carry 1/2/3 here."""
128    cleaning_result: YXCleaningResult | None = None
129    """How the clean ended: 0 interrupted (fault), 1 completed, 2 stopped (no fault)."""
130    start_method: YXStartMethod | None = None
131    """What initiated the clean: 0 remote, 1 app, 2 timer, 3 button."""
132    collect_dust_count: int | None = None
133    """Number of dock auto-empties during the clean."""
134
135    @property
136    def start_datetime(self) -> datetime.datetime | None:
137        """The start time as a timezone-aware (UTC) datetime."""
138        if self.start_time is not None:
139            return datetime.datetime.fromtimestamp(self.start_time).astimezone(datetime.UTC)
140        return None

A single Q10 (ss07) clean record decoded from a dpCleanRecord (DP 52) entry.

The device returns each record as a 12-field underscore-delimited string in the data list of a {"op": "list"} query (or the id of an {"op": "notify"} push). The *_len values are internal blob-length metrics whose units aren't confirmed; the original raw string is always retained. The enum fields resolve an unmapped/unset code to None rather than guessing.

Q10CleanRecord( raw: str, record_id: str | None = None, start_time: int | None = None, clean_time: int | None = None, clean_area: int | None = None, map_len: int | None = None, path_len: int | None = None, virtual_len: int | None = None, clean_mode: roborock.data.b01_q10.b01_q10_code_mappings.YXCleanScope | None = None, work_mode: roborock.data.b01_q10.b01_q10_code_mappings.YXCleanType | None = None, cleaning_result: roborock.data.b01_q10.b01_q10_code_mappings.YXCleaningResult | None = None, start_method: roborock.data.b01_q10.b01_q10_code_mappings.YXStartMethod | None = None, collect_dust_count: int | None = None)
raw: str
record_id: str | None = None
start_time: int | None = None

Clean start time, Unix seconds.

clean_time: int | None = None

Cleaning time, minutes.

clean_area: int | None = None

Cleaned area in square meters.

map_len: int | None = None

Length of the saved map blob for this record (0 = none stored).

path_len: int | None = None

Length of the saved path blob for this record (0 = none stored).

virtual_len: int | None = None

Length of the saved virtual-restriction blob for this record (0 = none stored).

Clean scope/type (full / selective-room / zone / spot). Same axis as the live YXDeviceCleanTask but a different record encoding -- see YXCleanScope.

Actual work performed (vac+mop / vacuum / mop) -- the same enum Q10Status uses for the live clean-mode DP. Records only ever carry 1/2/3 here.

How the clean ended: 0 interrupted (fault), 1 completed, 2 stopped (no fault).

What initiated the clean: 0 remote, 1 app, 2 timer, 3 button.

collect_dust_count: int | None = None

Number of dock auto-empties during the clean.

start_datetime: datetime.datetime | None
135    @property
136    def start_datetime(self) -> datetime.datetime | None:
137        """The start time as a timezone-aware (UTC) datetime."""
138        if self.start_time is not None:
139            return datetime.datetime.fromtimestamp(self.start_time).astimezone(datetime.UTC)
140        return None

The start time as a timezone-aware (UTC) datetime.

@dataclass
class Q10MapInfo(roborock.data.containers.RoborockBase):
143@dataclass
144class Q10MapInfo(RoborockBase):
145    """A saved map reported by ``dpMultiMap``.
146
147    Q10 firmware represents the map identifier as a string on the wire. The
148    value is sent back unchanged in a subsequent ``{"op": "select"}`` detail
149    request. On Q10 firmware, ``select`` previews a saved map without applying
150    it as the active map.
151    """
152
153    id: str
154    name: str | None = None
155    timestamp: int | None = None

A saved map reported by dpMultiMap.

Q10 firmware represents the map identifier as a string on the wire. The value is sent back unchanged in a subsequent {"op": "select"} detail request. On Q10 firmware, select previews a saved map without applying it as the active map.

Q10MapInfo(id: str, name: str | None = None, timestamp: int | None = None)
id: str
name: str | None = None
timestamp: int | None = None
@dataclass
class dpMultiMap(roborock.data.containers.RoborockBase):
158@dataclass
159class dpMultiMap(RoborockBase):
160    """Response envelope for the Q10 ``dpMultiMap`` data point."""
161
162    op: str
163    result: int
164    data: list[Q10MapInfo] = field(default_factory=list)
165
166    @property
167    def current_map_id(self) -> str | None:
168        """Return the first saved-map identifier, if one was reported."""
169        first = next((map_info for map_info in self.data if map_info.id), None)
170        return first.id if first else None

Response envelope for the Q10 dpMultiMap data point.

dpMultiMap( op: str, result: int, data: list[Q10MapInfo] = <factory>)
op: str
result: int
data: list[Q10MapInfo]
current_map_id: str | None
166    @property
167    def current_map_id(self) -> str | None:
168        """Return the first saved-map identifier, if one was reported."""
169        first = next((map_info for map_info in self.data if map_info.id), None)
170        return first.id if first else None

Return the first saved-map identifier, if one was reported.

@dataclass
class dpGetCarpet(roborock.data.containers.RoborockBase):
173@dataclass
174class dpGetCarpet(RoborockBase):
175    op: str
176    result: int
177    data: str
dpGetCarpet(op: str, result: int, data: str)
op: str
result: int
data: str
@dataclass
class dpSelfIdentifyingCarpet(roborock.data.containers.RoborockBase):
180@dataclass
181class dpSelfIdentifyingCarpet(RoborockBase):
182    op: str
183    result: int
184    data: str
dpSelfIdentifyingCarpet(op: str, result: int, data: str)
op: str
result: int
data: str
@dataclass
class dpNetInfo(roborock.data.containers.RoborockBase):
187@dataclass
188class dpNetInfo(RoborockBase):
189    wifi_name: str | None = None
190    # "ip_adress" intentionally mirrors the device's "ipAdress" key (sic).
191    ip_adress: str | None = None
192    mac: str | None = None
193    signal: int | None = None
194
195    @property
196    def ip_address(self) -> str | None:
197        """Correctly-spelled alias for :attr:`ip_adress`."""
198        return self.ip_adress
dpNetInfo( wifi_name: str | None = None, ip_adress: str | None = None, mac: str | None = None, signal: int | None = None)
wifi_name: str | None = None
ip_adress: str | None = None
mac: str | None = None
signal: int | None = None
ip_address: str | None
195    @property
196    def ip_address(self) -> str | None:
197        """Correctly-spelled alias for :attr:`ip_adress`."""
198        return self.ip_adress

Correctly-spelled alias for ip_adress.

@dataclass
class dpNotDisturbExpand(roborock.data.containers.RoborockBase):
201@dataclass
202class dpNotDisturbExpand(RoborockBase):
203    disturb_dust_enable: int | None = None
204    disturb_light: int | None = None
205    disturb_resume_clean: int | None = None
206    disturb_voice: int | None = None
dpNotDisturbExpand( disturb_dust_enable: int | None = None, disturb_light: int | None = None, disturb_resume_clean: int | None = None, disturb_voice: int | None = None)
disturb_dust_enable: int | None = None
disturb_light: int | None = None
disturb_resume_clean: int | None = None
disturb_voice: int | None = None
@dataclass
class dpCurrentCleanRoomIds(roborock.data.containers.RoborockBase):
209@dataclass
210class dpCurrentCleanRoomIds(RoborockBase):
211    room_id_list: list
dpCurrentCleanRoomIds(room_id_list: list)
room_id_list: list
@dataclass
class dpVoiceVersion(roborock.data.containers.RoborockBase):
214@dataclass
215class dpVoiceVersion(RoborockBase):
216    version: int
dpVoiceVersion(version: int)
version: int
@dataclass
class dpTimeZone(roborock.data.containers.RoborockBase):
219@dataclass
220class dpTimeZone(RoborockBase):
221    time_zone_city: str | None = None
222    time_zone_sec: int | None = None
dpTimeZone(time_zone_city: str | None = None, time_zone_sec: int | None = None)
time_zone_city: str | None = None
time_zone_sec: int | None = None
@dataclass
class Q10Status(roborock.data.containers.RoborockBase):
225@dataclass
226class Q10Status(RoborockBase):
227    """Core vacuum status for Q10 devices.
228
229    Fields are mapped to DPS values using metadata. Objects of this class can be
230    automatically updated using the `UpdatableTrait` helper. Settings that have
231    their own trait (volume, child lock, do-not-disturb, dust collection,
232    network info, consumables) live on those traits instead of here.
233    """
234
235    clean_time: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TIME})
236    clean_area: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_AREA})
237    battery: int | None = field(default=None, metadata={"dps": B01_Q10_DP.BATTERY})
238    status: YXDeviceState | None = field(default=None, metadata={"dps": B01_Q10_DP.STATUS})
239    fan_level: YXFanLevel | None = field(default=None, metadata={"dps": B01_Q10_DP.FAN_LEVEL})
240    water_level: YXWaterLevel | None = field(default=None, metadata={"dps": B01_Q10_DP.WATER_LEVEL})
241    clean_count: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_COUNT})
242    total_clean_area: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_AREA})
243    total_clean_count: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_COUNT})
244    total_clean_time: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_TIME})
245    clean_mode: YXCleanType | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_MODE})
246    clean_task_type: YXDeviceCleanTask | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TASK_TYPE})
247    back_type: YXBackType | None = field(default=None, metadata={"dps": B01_Q10_DP.BACK_TYPE})
248    cleaning_progress: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_PROGRESS})
249    fault: YXFault | None = field(default=None, metadata={"dps": B01_Q10_DP.FAULT})
250
251    # Additional state reported in the device's full status dump.
252    clean_line: YXCleanLine | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_LINE})
253    carpet_clean_type: YXCarpetCleanType | None = field(default=None, metadata={"dps": B01_Q10_DP.CARPET_CLEAN_TYPE})
254    area_unit: YXAreaUnit | None = field(default=None, metadata={"dps": B01_Q10_DP.AREA_UNIT})
255    auto_boost: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.AUTO_BOOST})
256    multi_map_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MULTI_MAP_SWITCH})
257    map_save_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MAP_SAVE_SWITCH})
258    recent_clean_record: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.RECENT_CLEAN_RECORD})
259    valley_point_charging: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.VALLEY_POINT_CHARGING})
260    line_laser_obstacle_avoidance: bool | None = field(
261        default=None, metadata={"dps": B01_Q10_DP.LINE_LASER_OBSTACLE_AVOIDANCE}
262    )
263    # Whether a mop module is attached, and whether "clean along floor direction" is on.
264    mop_state: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MOP_STATE})
265    ground_clean: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.GROUND_CLEAN})
266    # True while an "add area" / re-clean (the app's draw-a-rectangle "re cleaning")
267    # request is in progress; pulses back to False once the robot has the area.
268    add_clean_state: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.ADD_CLEAN_STATE})
269    robot_country_code: str | None = field(default=None, metadata={"dps": B01_Q10_DP.ROBOT_COUNTRY_CODE})
270    time_zone: dpTimeZone | None = field(default=None, metadata={"dps": B01_Q10_DP.TIME_ZONE})
271
272    # TODO(#846): value mappings for these ints are not yet decoded (no app
273    # control found / internal / constant); keep as int until reverse-engineered.
274    breakpoint_clean: int | None = field(default=None, metadata={"dps": B01_Q10_DP.BREAKPOINT_CLEAN})
275    timer_type: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TIMER_TYPE})
276    user_plan: int | None = field(default=None, metadata={"dps": B01_Q10_DP.USER_PLAN})
277    robot_type: int | None = field(default=None, metadata={"dps": B01_Q10_DP.ROBOT_TYPE})
278
279    # DEPRECATED: consumable/accessory remaining-life now lives on the
280    # ``Q10Consumable`` trait. These aliases are kept here for backwards
281    # compatibility and will be removed in a follow-up release. See PR #846.
282    main_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.MAIN_BRUSH_LIFE})
283    side_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SIDE_BRUSH_LIFE})
284    filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
285    sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
286
287    @property
288    def fault_name(self) -> str | None:
289        """Returns the name of the current fault."""
290        return self.fault.value if self.fault is not None else None

Core vacuum status for Q10 devices.

Fields are mapped to DPS values using metadata. Objects of this class can be automatically updated using the UpdatableTrait helper. Settings that have their own trait (volume, child lock, do-not-disturb, dust collection, network info, consumables) live on those traits instead of here.

Q10Status( clean_time: int | None = None, clean_area: int | None = None, battery: int | None = None, status: roborock.data.b01_q10.b01_q10_code_mappings.YXDeviceState | None = None, fan_level: roborock.data.b01_q10.b01_q10_code_mappings.YXFanLevel | None = None, water_level: roborock.data.b01_q10.b01_q10_code_mappings.YXWaterLevel | None = None, clean_count: int | None = None, total_clean_area: int | None = None, total_clean_count: int | None = None, total_clean_time: int | None = None, clean_mode: roborock.data.b01_q10.b01_q10_code_mappings.YXCleanType | None = None, clean_task_type: roborock.data.b01_q10.b01_q10_code_mappings.YXDeviceCleanTask | None = None, back_type: roborock.data.b01_q10.b01_q10_code_mappings.YXBackType | None = None, cleaning_progress: int | None = None, fault: roborock.data.b01_q10.b01_q10_code_mappings.YXFault | None = None, clean_line: roborock.data.b01_q10.b01_q10_code_mappings.YXCleanLine | None = None, carpet_clean_type: roborock.data.b01_q10.b01_q10_code_mappings.YXCarpetCleanType | None = None, area_unit: roborock.data.b01_q10.b01_q10_code_mappings.YXAreaUnit | None = None, auto_boost: bool | None = None, multi_map_switch: bool | None = None, map_save_switch: bool | None = None, recent_clean_record: bool | None = None, valley_point_charging: bool | None = None, line_laser_obstacle_avoidance: bool | None = None, mop_state: bool | None = None, ground_clean: bool | None = None, add_clean_state: bool | None = None, robot_country_code: str | None = None, time_zone: dpTimeZone | None = None, breakpoint_clean: int | None = None, timer_type: int | None = None, user_plan: int | None = None, robot_type: int | None = None, main_brush_life: int | None = None, side_brush_life: int | None = None, filter_life: int | None = None, sensor_life: int | None = None)
clean_time: int | None = None
clean_area: int | None = None
battery: int | None = None
clean_count: int | None = None
total_clean_area: int | None = None
total_clean_count: int | None = None
total_clean_time: int | None = None
cleaning_progress: int | None = None
auto_boost: bool | None = None
multi_map_switch: bool | None = None
map_save_switch: bool | None = None
recent_clean_record: bool | None = None
valley_point_charging: bool | None = None
line_laser_obstacle_avoidance: bool | None = None
mop_state: bool | None = None
ground_clean: bool | None = None
add_clean_state: bool | None = None
robot_country_code: str | None = None
time_zone: dpTimeZone | None = None
breakpoint_clean: int | None = None
timer_type: int | None = None
user_plan: int | None = None
robot_type: int | None = None
main_brush_life: int | None = None
side_brush_life: int | None = None
filter_life: int | None = None
sensor_life: int | None = None
fault_name: str | None
287    @property
288    def fault_name(self) -> str | None:
289        """Returns the name of the current fault."""
290        return self.fault.value if self.fault is not None else None

Returns the name of the current fault.

@dataclass
class SoundVolume(roborock.data.containers.RoborockBase):
293@dataclass
294class SoundVolume(RoborockBase):
295    """Speaker volume read-model (0-100)."""
296
297    volume: int | None = field(default=None, metadata={"dps": B01_Q10_DP.VOLUME})

Speaker volume read-model (0-100).

SoundVolume(volume: int | None = None)
volume: int | None = None
@dataclass
class ChildLock(roborock.data.containers.RoborockBase):
300@dataclass
301class ChildLock(RoborockBase):
302    """Child-lock read-model."""
303
304    child_lock: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.CHILD_LOCK})

Child-lock read-model.

ChildLock(child_lock: bool | None = None)
child_lock: bool | None = None
@dataclass
class DoNotDisturb(roborock.data.containers.RoborockBase):
307@dataclass
308class DoNotDisturb(RoborockBase):
309    """Do Not Disturb read-model."""
310
311    not_disturb: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.NOT_DISTURB})
312    not_disturb_expand: dpNotDisturbExpand | None = field(default=None, metadata={"dps": B01_Q10_DP.NOT_DISTURB_EXPAND})

Do Not Disturb read-model.

DoNotDisturb( not_disturb: bool | None = None, not_disturb_expand: dpNotDisturbExpand | None = None)
not_disturb: bool | None = None
not_disturb_expand: dpNotDisturbExpand | None = None
@dataclass
class DustCollection(roborock.data.containers.RoborockBase):
315@dataclass
316class DustCollection(RoborockBase):
317    """Dock auto-empty (dust collection) read-model."""
318
319    dust_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.DUST_SWITCH})
320    dust_setting: YXDeviceDustCollectionFrequency | None = field(
321        default=None, metadata={"dps": B01_Q10_DP.DUST_SETTING}
322    )

Dock auto-empty (dust collection) read-model.

DustCollection( dust_switch: bool | None = None, dust_setting: roborock.data.b01_q10.b01_q10_code_mappings.YXDeviceDustCollectionFrequency | None = None)
dust_switch: bool | None = None
@dataclass
class Q10Consumable(roborock.data.containers.RoborockBase):
325@dataclass
326class Q10Consumable(RoborockBase):
327    """Consumable / accessory remaining-life read-model.
328
329    Named with a ``Q10`` prefix to avoid shadowing the v1 ``Consumable`` when both
330    are star-imported into the ``roborock.data`` namespace.
331    """
332
333    main_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.MAIN_BRUSH_LIFE})
334    side_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SIDE_BRUSH_LIFE})
335    filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
336    sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})

Consumable / accessory remaining-life read-model.

Named with a Q10 prefix to avoid shadowing the v1 Consumable when both are star-imported into the roborock.data namespace.

Q10Consumable( main_brush_life: int | None = None, side_brush_life: int | None = None, filter_life: int | None = None, sensor_life: int | None = None)
main_brush_life: int | None = None
side_brush_life: int | None = None
filter_life: int | None = None
sensor_life: int | None = None
@dataclass
class Q10NetworkInfo(roborock.data.containers.RoborockBase):
339@dataclass
340class Q10NetworkInfo(RoborockBase):
341    """Network information read-model.
342
343    Named with a ``Q10`` prefix to avoid shadowing the v1 ``NetworkInfo`` when both
344    are star-imported into the ``roborock.data`` namespace.
345    """
346
347    net_info: dpNetInfo | None = field(default=None, metadata={"dps": B01_Q10_DP.NET_INFO})

Network information read-model.

Named with a Q10 prefix to avoid shadowing the v1 NetworkInfo when both are star-imported into the roborock.data namespace.

Q10NetworkInfo( net_info: dpNetInfo | None = None)
net_info: dpNetInfo | None = None