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": "get"}`` request.
148    """
149
150    id: str
151    name: str | None = None
152    timestamp: int | None = None
153
154
155@dataclass
156class dpMultiMap(RoborockBase):
157    """Response envelope for the Q10 ``dpMultiMap`` data point."""
158
159    op: str
160    result: int
161    data: list[Q10MapInfo] = field(default_factory=list)
162
163    @property
164    def current_map_id(self) -> str | None:
165        """Return the first saved-map identifier, if one was reported."""
166        first = next((map_info for map_info in self.data if map_info.id), None)
167        return first.id if first else None
168
169
170@dataclass
171class dpGetCarpet(RoborockBase):
172    op: str
173    result: int
174    data: str
175
176
177@dataclass
178class dpSelfIdentifyingCarpet(RoborockBase):
179    op: str
180    result: int
181    data: str
182
183
184@dataclass
185class dpNetInfo(RoborockBase):
186    wifi_name: str | None = None
187    # "ip_adress" intentionally mirrors the device's "ipAdress" key (sic).
188    ip_adress: str | None = None
189    mac: str | None = None
190    signal: int | None = None
191
192    @property
193    def ip_address(self) -> str | None:
194        """Correctly-spelled alias for :attr:`ip_adress`."""
195        return self.ip_adress
196
197
198@dataclass
199class dpNotDisturbExpand(RoborockBase):
200    disturb_dust_enable: int | None = None
201    disturb_light: int | None = None
202    disturb_resume_clean: int | None = None
203    disturb_voice: int | None = None
204
205
206@dataclass
207class dpCurrentCleanRoomIds(RoborockBase):
208    room_id_list: list
209
210
211@dataclass
212class dpVoiceVersion(RoborockBase):
213    version: int
214
215
216@dataclass
217class dpTimeZone(RoborockBase):
218    time_zone_city: str | None = None
219    time_zone_sec: int | None = None
220
221
222@dataclass
223class Q10Status(RoborockBase):
224    """Core vacuum status for Q10 devices.
225
226    Fields are mapped to DPS values using metadata. Objects of this class can be
227    automatically updated using the `UpdatableTrait` helper. Settings that have
228    their own trait (volume, child lock, do-not-disturb, dust collection,
229    network info, consumables) live on those traits instead of here.
230    """
231
232    clean_time: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TIME})
233    clean_area: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_AREA})
234    battery: int | None = field(default=None, metadata={"dps": B01_Q10_DP.BATTERY})
235    status: YXDeviceState | None = field(default=None, metadata={"dps": B01_Q10_DP.STATUS})
236    fan_level: YXFanLevel | None = field(default=None, metadata={"dps": B01_Q10_DP.FAN_LEVEL})
237    water_level: YXWaterLevel | None = field(default=None, metadata={"dps": B01_Q10_DP.WATER_LEVEL})
238    clean_count: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_COUNT})
239    total_clean_area: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_AREA})
240    total_clean_count: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_COUNT})
241    total_clean_time: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TOTAL_CLEAN_TIME})
242    clean_mode: YXCleanType | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_MODE})
243    clean_task_type: YXDeviceCleanTask | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TASK_TYPE})
244    back_type: YXBackType | None = field(default=None, metadata={"dps": B01_Q10_DP.BACK_TYPE})
245    cleaning_progress: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_PROGRESS})
246    fault: YXFault | None = field(default=None, metadata={"dps": B01_Q10_DP.FAULT})
247
248    # Additional state reported in the device's full status dump.
249    clean_line: YXCleanLine | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_LINE})
250    carpet_clean_type: YXCarpetCleanType | None = field(default=None, metadata={"dps": B01_Q10_DP.CARPET_CLEAN_TYPE})
251    area_unit: YXAreaUnit | None = field(default=None, metadata={"dps": B01_Q10_DP.AREA_UNIT})
252    auto_boost: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.AUTO_BOOST})
253    multi_map_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MULTI_MAP_SWITCH})
254    map_save_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MAP_SAVE_SWITCH})
255    recent_clean_record: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.RECENT_CLEAN_RECORD})
256    valley_point_charging: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.VALLEY_POINT_CHARGING})
257    line_laser_obstacle_avoidance: bool | None = field(
258        default=None, metadata={"dps": B01_Q10_DP.LINE_LASER_OBSTACLE_AVOIDANCE}
259    )
260    # Whether a mop module is attached, and whether "clean along floor direction" is on.
261    mop_state: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.MOP_STATE})
262    ground_clean: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.GROUND_CLEAN})
263    # True while an "add area" / re-clean (the app's draw-a-rectangle "re cleaning")
264    # request is in progress; pulses back to False once the robot has the area.
265    add_clean_state: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.ADD_CLEAN_STATE})
266    robot_country_code: str | None = field(default=None, metadata={"dps": B01_Q10_DP.ROBOT_COUNTRY_CODE})
267    time_zone: dpTimeZone | None = field(default=None, metadata={"dps": B01_Q10_DP.TIME_ZONE})
268
269    # TODO(#846): value mappings for these ints are not yet decoded (no app
270    # control found / internal / constant); keep as int until reverse-engineered.
271    breakpoint_clean: int | None = field(default=None, metadata={"dps": B01_Q10_DP.BREAKPOINT_CLEAN})
272    timer_type: int | None = field(default=None, metadata={"dps": B01_Q10_DP.TIMER_TYPE})
273    user_plan: int | None = field(default=None, metadata={"dps": B01_Q10_DP.USER_PLAN})
274    robot_type: int | None = field(default=None, metadata={"dps": B01_Q10_DP.ROBOT_TYPE})
275
276    # DEPRECATED: consumable/accessory remaining-life now lives on the
277    # ``Q10Consumable`` trait. These aliases are kept here for backwards
278    # compatibility and will be removed in a follow-up release. See PR #846.
279    main_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.MAIN_BRUSH_LIFE})
280    side_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SIDE_BRUSH_LIFE})
281    filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
282    sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
283
284    @property
285    def fault_name(self) -> str | None:
286        """Returns the name of the current fault."""
287        return self.fault.value if self.fault is not None else None
288
289
290@dataclass
291class SoundVolume(RoborockBase):
292    """Speaker volume read-model (0-100)."""
293
294    volume: int | None = field(default=None, metadata={"dps": B01_Q10_DP.VOLUME})
295
296
297@dataclass
298class ChildLock(RoborockBase):
299    """Child-lock read-model."""
300
301    child_lock: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.CHILD_LOCK})
302
303
304@dataclass
305class DoNotDisturb(RoborockBase):
306    """Do Not Disturb read-model."""
307
308    not_disturb: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.NOT_DISTURB})
309    not_disturb_expand: dpNotDisturbExpand | None = field(default=None, metadata={"dps": B01_Q10_DP.NOT_DISTURB_EXPAND})
310
311
312@dataclass
313class DustCollection(RoborockBase):
314    """Dock auto-empty (dust collection) read-model."""
315
316    dust_switch: bool | None = field(default=None, metadata={"dps": B01_Q10_DP.DUST_SWITCH})
317    dust_setting: YXDeviceDustCollectionFrequency | None = field(
318        default=None, metadata={"dps": B01_Q10_DP.DUST_SETTING}
319    )
320
321
322@dataclass
323class Q10Consumable(RoborockBase):
324    """Consumable / accessory remaining-life read-model.
325
326    Named with a ``Q10`` prefix to avoid shadowing the v1 ``Consumable`` when both
327    are star-imported into the ``roborock.data`` namespace.
328    """
329
330    main_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.MAIN_BRUSH_LIFE})
331    side_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SIDE_BRUSH_LIFE})
332    filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
333    sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
334
335
336@dataclass
337class Q10NetworkInfo(RoborockBase):
338    """Network information read-model.
339
340    Named with a ``Q10`` prefix to avoid shadowing the v1 ``NetworkInfo`` when both
341    are star-imported into the ``roborock.data`` namespace.
342    """
343
344    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": "get"}`` request.
149    """
150
151    id: str
152    name: str | None = None
153    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": "get"} request.

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

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

@dataclass
class dpGetCarpet(roborock.data.containers.RoborockBase):
171@dataclass
172class dpGetCarpet(RoborockBase):
173    op: str
174    result: int
175    data: str
dpGetCarpet(op: str, result: int, data: str)
op: str
result: int
data: str
@dataclass
class dpSelfIdentifyingCarpet(roborock.data.containers.RoborockBase):
178@dataclass
179class dpSelfIdentifyingCarpet(RoborockBase):
180    op: str
181    result: int
182    data: str
dpSelfIdentifyingCarpet(op: str, result: int, data: str)
op: str
result: int
data: str
@dataclass
class dpNetInfo(roborock.data.containers.RoborockBase):
185@dataclass
186class dpNetInfo(RoborockBase):
187    wifi_name: str | None = None
188    # "ip_adress" intentionally mirrors the device's "ipAdress" key (sic).
189    ip_adress: str | None = None
190    mac: str | None = None
191    signal: int | None = None
192
193    @property
194    def ip_address(self) -> str | None:
195        """Correctly-spelled alias for :attr:`ip_adress`."""
196        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
193    @property
194    def ip_address(self) -> str | None:
195        """Correctly-spelled alias for :attr:`ip_adress`."""
196        return self.ip_adress

Correctly-spelled alias for ip_adress.

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

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):
323@dataclass
324class Q10Consumable(RoborockBase):
325    """Consumable / accessory remaining-life read-model.
326
327    Named with a ``Q10`` prefix to avoid shadowing the v1 ``Consumable`` when both
328    are star-imported into the ``roborock.data`` namespace.
329    """
330
331    main_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.MAIN_BRUSH_LIFE})
332    side_brush_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SIDE_BRUSH_LIFE})
333    filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
334    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):
337@dataclass
338class Q10NetworkInfo(RoborockBase):
339    """Network information read-model.
340
341    Named with a ``Q10`` prefix to avoid shadowing the v1 ``NetworkInfo`` when both
342    are star-imported into the ``roborock.data`` namespace.
343    """
344
345    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