roborock.data.containers

  1import dataclasses
  2import datetime
  3import inspect
  4import json
  5import logging
  6import re
  7import types
  8from dataclasses import asdict, dataclass, field
  9from enum import Enum
 10from functools import cached_property
 11from typing import Any, ClassVar, NamedTuple, get_args, get_origin
 12
 13from .code_mappings import (
 14    SHORT_MODEL_TO_ENUM,
 15    RoborockCategory,
 16    RoborockModeEnum,
 17    RoborockProductNickname,
 18)
 19
 20_LOGGER = logging.getLogger(__name__)
 21
 22
 23def _camelize(s: str):
 24    first, *others = s.split("_")
 25    if len(others) == 0:
 26        return s
 27    return "".join([first.lower(), *map(str.title, others)])
 28
 29
 30def _decamelize(s: str):
 31    # Split before uppercase letters not at the start, and before numbers
 32    s = re.sub(r"(?<=[a-z0-9])([A-Z])", r"_\1", s)
 33    s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s)  # Split acronyms followed by normal camelCase
 34    s = re.sub(r"([a-zA-Z])([0-9]+)", r"\1_\2", s)
 35    s = s.lower()
 36    # Temporary fix to avoid breaking any serialization.
 37    s = s.replace("base_64", "base64")
 38    return s
 39
 40
 41def _attr_repr(obj: Any) -> str:
 42    """Return a string representation of the object including specified attributes.
 43
 44    This reproduces the default repr behavior of dataclasses, but also includes
 45    properties. This must be called by the child class's __repr__ method since
 46    the parent RoborockBase class does not know about the child class's attributes.
 47    """
 48    # Reproduce default repr behavior
 49    parts = []
 50    for k in dir(obj):
 51        if k.startswith("_"):
 52            continue
 53        try:
 54            v = getattr(obj, k)
 55        except (RuntimeError, Exception):
 56            continue
 57        if callable(v):
 58            continue
 59        parts.append(f"{k}={v!r}")
 60    return f"{type(obj).__name__}({', '.join(parts)})"
 61
 62
 63@dataclass(repr=False)
 64class RoborockBase:
 65    """Base class for all Roborock data classes."""
 66
 67    _missing_logged: ClassVar[set[str]] = set()
 68
 69    @staticmethod
 70    def _convert_to_class_obj(class_type: type, value):
 71        if get_origin(class_type) is list:
 72            sub_type = get_args(class_type)[0]
 73            return [RoborockBase._convert_to_class_obj(sub_type, obj) for obj in value]
 74        if get_origin(class_type) is dict:
 75            key_type, value_type = get_args(class_type)
 76            if key_type is not None:
 77                return {key_type(k): RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()}
 78            return {k: RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()}
 79        if inspect.isclass(class_type):
 80            if issubclass(class_type, RoborockBase):
 81                return class_type.from_dict(value)
 82            if issubclass(class_type, RoborockModeEnum):
 83                return class_type.from_code(value)
 84        if class_type is Any or type(class_type) is str:
 85            return value
 86        return class_type(value)  # type: ignore[call-arg]
 87
 88    @classmethod
 89    def from_dict(cls, data: dict[str, Any]):
 90        """Create an instance of the class from a dictionary."""
 91        if not isinstance(data, dict):
 92            return None
 93        field_types = {field.name: field.type for field in dataclasses.fields(cls)}
 94        normalized_data: dict[str, Any] = {}
 95        for orig_key, value in data.items():
 96            key = _decamelize(orig_key)
 97            if field_types.get(key) is None:
 98                if (log_key := f"{cls.__name__}.{key}") not in RoborockBase._missing_logged:
 99                    _LOGGER.debug(
100                        "Key '%s' (decamelized: '%s') not found in %s fields, skipping",
101                        orig_key,
102                        key,
103                        cls.__name__,
104                    )
105                    RoborockBase._missing_logged.add(log_key)
106                continue
107            normalized_data[key] = value
108
109        result = RoborockBase.convert_dict(field_types, normalized_data)
110        return cls(**result)
111
112    @staticmethod
113    def convert_dict(types_map: dict[Any, type], data: dict[Any, Any]) -> dict[Any, Any]:
114        """Generic helper to convert a dictionary of values based on a schema map of types.
115
116        This is meant to be used by traits that use dataclass reflection similar to
117        `Roborock.from_dict` to merge in new data updates.
118        """
119        result: dict[Any, Any] = {}
120        for key, value in data.items():
121            if key not in types_map:
122                continue
123            field_type = types_map[key]
124            if value == "None" or value is None:
125                result[key] = None
126                continue
127            if isinstance(field_type, types.UnionType):
128                for subtype in get_args(field_type):
129                    if subtype is types.NoneType:
130                        continue
131                    try:
132                        result[key] = RoborockBase._convert_to_class_obj(subtype, value)
133                        break
134                    except Exception:
135                        _LOGGER.exception(f"Failed to convert {key} with value {value} to type {subtype}")
136                        continue
137            else:
138                try:
139                    result[key] = RoborockBase._convert_to_class_obj(field_type, value)
140                except Exception:
141                    _LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}")
142                    continue
143
144        return result
145
146    def as_dict(self) -> dict:
147        return asdict(
148            self,
149            dict_factory=lambda _fields: {
150                _camelize(key): value.value if isinstance(value, Enum) else value
151                for (key, value) in _fields
152                if value is not None
153            },
154        )
155
156
157@dataclass
158class RoborockBaseTimer(RoborockBase):
159    start_hour: int | None = None
160    start_minute: int | None = None
161    end_hour: int | None = None
162    end_minute: int | None = None
163    enabled: int | None = None
164
165    @property
166    def start_time(self) -> datetime.time | None:
167        return (
168            datetime.time(hour=self.start_hour, minute=self.start_minute)
169            if self.start_hour is not None and self.start_minute is not None
170            else None
171        )
172
173    @property
174    def end_time(self) -> datetime.time | None:
175        return (
176            datetime.time(hour=self.end_hour, minute=self.end_minute)
177            if self.end_hour is not None and self.end_minute is not None
178            else None
179        )
180
181    def as_list(self) -> list:
182        return [self.start_hour, self.start_minute, self.end_hour, self.end_minute]
183
184    def __repr__(self) -> str:
185        return _attr_repr(self)
186
187
188@dataclass
189class Reference(RoborockBase):
190    r: str | None = None
191    a: str | None = None
192    m: str | None = None
193    l: str | None = None
194
195
196@dataclass
197class RRiot(RoborockBase):
198    u: str
199    s: str
200    h: str
201    k: str
202    r: Reference
203
204
205@dataclass
206class UserData(RoborockBase):
207    rriot: RRiot
208    uid: int | None = None
209    tokentype: str | None = None
210    token: str | None = None
211    rruid: str | None = None
212    region: str | None = None
213    countrycode: str | None = None
214    country: str | None = None
215    nickname: str | None = None
216    tuya_device_state: int | None = None
217    avatarurl: str | None = None
218
219
220@dataclass
221class HomeDataProductSchema(RoborockBase):
222    id: Any | None = None
223    name: Any | None = None
224    code: Any | None = None
225    mode: Any | None = None
226    type: Any | None = None
227    product_property: Any | None = None
228    property: Any | None = None
229    desc: Any | None = None
230
231
232@dataclass
233class HomeDataProduct(RoborockBase):
234    id: str
235    name: str
236    model: str
237    category: RoborockCategory
238    code: str | None = None
239    icon_url: str | None = None
240    attribute: Any | None = None
241    capability: int | None = None
242    schema: list[HomeDataProductSchema] | None = None
243
244    @property
245    def product_nickname(self) -> RoborockProductNickname:
246        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
247
248    def summary_info(self) -> str:
249        """Return a string with key product information for logging purposes."""
250        return f"{self.name} (model={self.model}, category={self.category})"
251
252    @cached_property
253    def supported_schema_codes(self) -> set[str]:
254        """Return a set of fields that are supported by the device."""
255        if self.schema is None:
256            return set()
257        return {schema.code for schema in self.schema if schema.code is not None}
258
259
260@dataclass
261class HomeDataDevice(RoborockBase):
262    duid: str
263    name: str
264    local_key: str
265    product_id: str
266    fv: str | None = None
267    attribute: Any | None = None
268    active_time: int | None = None
269    runtime_env: Any | None = None
270    time_zone_id: str | None = None
271    icon_url: str | None = None
272    lon: Any | None = None
273    lat: Any | None = None
274    share: Any | None = None
275    share_time: Any | None = None
276    online: bool | None = None
277    pv: str | None = None
278    room_id: Any | None = None
279    tuya_uuid: Any | None = None
280    tuya_migrated: bool | None = None
281    extra: Any | None = None
282    sn: str | None = None
283    feature_set: str | None = None
284    new_feature_set: str | None = None
285    device_status: dict | None = None
286    silent_ota_switch: bool | None = None
287    setting: Any | None = None
288    f: bool | None = None
289    create_time: int | None = None
290    cid: str | None = None
291    share_type: Any | None = None
292    share_expired_time: int | None = None
293
294    def summary_info(self) -> str:
295        """Return a string with key device information for logging purposes."""
296        return f"{self.name} (pv={self.pv}, fv={self.fv}, online={self.online})"
297
298
299@dataclass
300class HomeDataRoom(RoborockBase):
301    id: int
302    name: str
303
304    @property
305    def iot_id(self) -> str:
306        """Return the room's ID as a string IOT ID."""
307        return str(self.id)
308
309
310@dataclass
311class HomeDataScene(RoborockBase):
312    id: int
313    name: str
314
315
316@dataclass
317class HomeDataSchedule(RoborockBase):
318    id: int
319    cron: str
320    repeated: bool
321    enabled: bool
322    param: dict | None = None
323
324
325@dataclass
326class HomeData(RoborockBase):
327    id: int
328    name: str
329    products: list[HomeDataProduct] = field(default_factory=lambda: [])
330    devices: list[HomeDataDevice] = field(default_factory=lambda: [])
331    received_devices: list[HomeDataDevice] = field(default_factory=lambda: [])
332    lon: Any | None = None
333    lat: Any | None = None
334    geo_name: Any | None = None
335    rooms: list[HomeDataRoom] = field(default_factory=list)
336
337    def get_all_devices(self) -> list[HomeDataDevice]:
338        devices = []
339        if self.devices is not None:
340            devices += self.devices
341        if self.received_devices is not None:
342            devices += self.received_devices
343        return devices
344
345    @cached_property
346    def product_map(self) -> dict[str, HomeDataProduct]:
347        """Returns a dictionary of product IDs to HomeDataProduct objects."""
348        return {product.id: product for product in self.products}
349
350    @cached_property
351    def device_products(self) -> dict[str, tuple[HomeDataDevice, HomeDataProduct]]:
352        """Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects."""
353        product_map = self.product_map
354        return {
355            device.duid: (device, product)
356            for device in self.get_all_devices()
357            if (product := product_map.get(device.product_id)) is not None
358        }
359
360    @property
361    def rooms_map(self) -> dict[str, HomeDataRoom]:
362        """Returns a dictionary of Room iot_id to rooms"""
363        return {room.iot_id: room for room in self.rooms}
364
365    @property
366    def rooms_name_map(self) -> dict[str, str]:
367        """Returns a dictionary of Room iot_id to room names."""
368        return {room.iot_id: room.name for room in self.rooms}
369
370
371@dataclass
372class LoginData(RoborockBase):
373    user_data: UserData
374    email: str
375    home_data: HomeData | None = None
376
377
378@dataclass
379class DeviceData(RoborockBase):
380    device: HomeDataDevice
381    model: str
382    host: str | None = None
383
384    @property
385    def product_nickname(self) -> RoborockProductNickname:
386        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
387
388    def __repr__(self) -> str:
389        return _attr_repr(self)
390
391
392@dataclass
393class RoomMapping(RoborockBase):
394    segment_id: int
395    iot_id: str
396
397
398@dataclass
399class NamedRoomMapping(RoomMapping):
400    """Dataclass representing a mapping of a room segment to a name.
401
402    The name information is not provided by the device directly, but is provided
403    from the HomeData based on the iot_id from the room.
404    """
405
406    @property
407    def name(self) -> str:
408        """The human-readable name of the room, or a default name if not available."""
409        return self.raw_name or f"Room {self.segment_id}"
410
411    raw_name: str | None = None
412    """The raw name of the room, as provided by the device."""
413
414
415@dataclass
416class CombinedMapInfo(RoborockBase):
417    """Data structure for caching home information.
418
419    This is not provided directly by the API, but is a combination of map data
420    and room data to provide a more useful structure.
421    """
422
423    map_flag: int
424    """The map identifier."""
425
426    name: str
427    """The name of the map from MultiMapsListMapInfo."""
428
429    rooms: list[NamedRoomMapping]
430    """The list of rooms in the map."""
431
432    @property
433    def rooms_map(self) -> dict[int, NamedRoomMapping]:
434        """Returns a mapping of segment_id to NamedRoomMapping."""
435        return {room.segment_id: room for room in self.rooms}
436
437
438@dataclass
439class BroadcastMessage(RoborockBase):
440    duid: str
441    ip: str
442    version: bytes
443
444
445class ServerTimer(NamedTuple):
446    id: str
447    status: str
448    dontknow: int
449
450
451@dataclass
452class RoborockProductStateValue(RoborockBase):
453    value: list
454    desc: dict
455
456
457@dataclass
458class RoborockProductState(RoborockBase):
459    dps: int
460    desc: dict
461    value: list[RoborockProductStateValue]
462
463
464@dataclass
465class RoborockProductSpec(RoborockBase):
466    state: RoborockProductState
467    battery: dict | None = None
468    dry_countdown: dict | None = None
469    extra: dict | None = None
470    offpeak: dict | None = None
471    countdown: dict | None = None
472    mode: dict | None = None
473    ota_nfo: dict | None = None
474    pause: dict | None = None
475    program: dict | None = None
476    shutdown: dict | None = None
477    washing_left: dict | None = None
478
479
480@dataclass
481class RoborockProduct(RoborockBase):
482    id: int | None = None
483    name: str | None = None
484    model: str | None = None
485    packagename: str | None = None
486    ssid: str | None = None
487    picurl: str | None = None
488    cardpicurl: str | None = None
489    mediumCardpicurl: str | None = None
490    resetwifipicurl: str | None = None
491    configPicUrl: str | None = None
492    pluginPicUrl: str | None = None
493    resetwifitext: dict | None = None
494    tuyaid: str | None = None
495    status: int | None = None
496    rriotid: str | None = None
497    pictures: list | None = None
498    ncMode: str | None = None
499    scope: str | None = None
500    product_tags: list | None = None
501    agreements: list | None = None
502    cardspec: str | None = None
503    plugin_pic_url: str | None = None
504
505    @property
506    def product_nickname(self) -> RoborockProductNickname | None:
507        if self.cardspec:
508            return RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data"))
509        return None
510
511    def __repr__(self) -> str:
512        return _attr_repr(self)
513
514
515@dataclass
516class RoborockProductCategory(RoborockBase):
517    id: int
518    display_name: str
519    icon_url: str
520
521
522@dataclass
523class RoborockCategoryDetail(RoborockBase):
524    category: RoborockProductCategory
525    product_list: list[RoborockProduct]
526
527
528@dataclass
529class ProductResponse(RoborockBase):
530    category_detail_list: list[RoborockCategoryDetail]
@dataclass(repr=False)
class RoborockBase:
 64@dataclass(repr=False)
 65class RoborockBase:
 66    """Base class for all Roborock data classes."""
 67
 68    _missing_logged: ClassVar[set[str]] = set()
 69
 70    @staticmethod
 71    def _convert_to_class_obj(class_type: type, value):
 72        if get_origin(class_type) is list:
 73            sub_type = get_args(class_type)[0]
 74            return [RoborockBase._convert_to_class_obj(sub_type, obj) for obj in value]
 75        if get_origin(class_type) is dict:
 76            key_type, value_type = get_args(class_type)
 77            if key_type is not None:
 78                return {key_type(k): RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()}
 79            return {k: RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()}
 80        if inspect.isclass(class_type):
 81            if issubclass(class_type, RoborockBase):
 82                return class_type.from_dict(value)
 83            if issubclass(class_type, RoborockModeEnum):
 84                return class_type.from_code(value)
 85        if class_type is Any or type(class_type) is str:
 86            return value
 87        return class_type(value)  # type: ignore[call-arg]
 88
 89    @classmethod
 90    def from_dict(cls, data: dict[str, Any]):
 91        """Create an instance of the class from a dictionary."""
 92        if not isinstance(data, dict):
 93            return None
 94        field_types = {field.name: field.type for field in dataclasses.fields(cls)}
 95        normalized_data: dict[str, Any] = {}
 96        for orig_key, value in data.items():
 97            key = _decamelize(orig_key)
 98            if field_types.get(key) is None:
 99                if (log_key := f"{cls.__name__}.{key}") not in RoborockBase._missing_logged:
100                    _LOGGER.debug(
101                        "Key '%s' (decamelized: '%s') not found in %s fields, skipping",
102                        orig_key,
103                        key,
104                        cls.__name__,
105                    )
106                    RoborockBase._missing_logged.add(log_key)
107                continue
108            normalized_data[key] = value
109
110        result = RoborockBase.convert_dict(field_types, normalized_data)
111        return cls(**result)
112
113    @staticmethod
114    def convert_dict(types_map: dict[Any, type], data: dict[Any, Any]) -> dict[Any, Any]:
115        """Generic helper to convert a dictionary of values based on a schema map of types.
116
117        This is meant to be used by traits that use dataclass reflection similar to
118        `Roborock.from_dict` to merge in new data updates.
119        """
120        result: dict[Any, Any] = {}
121        for key, value in data.items():
122            if key not in types_map:
123                continue
124            field_type = types_map[key]
125            if value == "None" or value is None:
126                result[key] = None
127                continue
128            if isinstance(field_type, types.UnionType):
129                for subtype in get_args(field_type):
130                    if subtype is types.NoneType:
131                        continue
132                    try:
133                        result[key] = RoborockBase._convert_to_class_obj(subtype, value)
134                        break
135                    except Exception:
136                        _LOGGER.exception(f"Failed to convert {key} with value {value} to type {subtype}")
137                        continue
138            else:
139                try:
140                    result[key] = RoborockBase._convert_to_class_obj(field_type, value)
141                except Exception:
142                    _LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}")
143                    continue
144
145        return result
146
147    def as_dict(self) -> dict:
148        return asdict(
149            self,
150            dict_factory=lambda _fields: {
151                _camelize(key): value.value if isinstance(value, Enum) else value
152                for (key, value) in _fields
153                if value is not None
154            },
155        )

Base class for all Roborock data classes.

@classmethod
def from_dict(cls, data: dict[str, typing.Any]):
 89    @classmethod
 90    def from_dict(cls, data: dict[str, Any]):
 91        """Create an instance of the class from a dictionary."""
 92        if not isinstance(data, dict):
 93            return None
 94        field_types = {field.name: field.type for field in dataclasses.fields(cls)}
 95        normalized_data: dict[str, Any] = {}
 96        for orig_key, value in data.items():
 97            key = _decamelize(orig_key)
 98            if field_types.get(key) is None:
 99                if (log_key := f"{cls.__name__}.{key}") not in RoborockBase._missing_logged:
100                    _LOGGER.debug(
101                        "Key '%s' (decamelized: '%s') not found in %s fields, skipping",
102                        orig_key,
103                        key,
104                        cls.__name__,
105                    )
106                    RoborockBase._missing_logged.add(log_key)
107                continue
108            normalized_data[key] = value
109
110        result = RoborockBase.convert_dict(field_types, normalized_data)
111        return cls(**result)

Create an instance of the class from a dictionary.

@staticmethod
def convert_dict( types_map: dict[typing.Any, type], data: dict[typing.Any, typing.Any]) -> dict[typing.Any, typing.Any]:
113    @staticmethod
114    def convert_dict(types_map: dict[Any, type], data: dict[Any, Any]) -> dict[Any, Any]:
115        """Generic helper to convert a dictionary of values based on a schema map of types.
116
117        This is meant to be used by traits that use dataclass reflection similar to
118        `Roborock.from_dict` to merge in new data updates.
119        """
120        result: dict[Any, Any] = {}
121        for key, value in data.items():
122            if key not in types_map:
123                continue
124            field_type = types_map[key]
125            if value == "None" or value is None:
126                result[key] = None
127                continue
128            if isinstance(field_type, types.UnionType):
129                for subtype in get_args(field_type):
130                    if subtype is types.NoneType:
131                        continue
132                    try:
133                        result[key] = RoborockBase._convert_to_class_obj(subtype, value)
134                        break
135                    except Exception:
136                        _LOGGER.exception(f"Failed to convert {key} with value {value} to type {subtype}")
137                        continue
138            else:
139                try:
140                    result[key] = RoborockBase._convert_to_class_obj(field_type, value)
141                except Exception:
142                    _LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}")
143                    continue
144
145        return result

Generic helper to convert a dictionary of values based on a schema map of types.

This is meant to be used by traits that use dataclass reflection similar to Roborock.from_dict to merge in new data updates.

def as_dict(self) -> dict:
147    def as_dict(self) -> dict:
148        return asdict(
149            self,
150            dict_factory=lambda _fields: {
151                _camelize(key): value.value if isinstance(value, Enum) else value
152                for (key, value) in _fields
153                if value is not None
154            },
155        )
@dataclass
class RoborockBaseTimer(RoborockBase):
158@dataclass
159class RoborockBaseTimer(RoborockBase):
160    start_hour: int | None = None
161    start_minute: int | None = None
162    end_hour: int | None = None
163    end_minute: int | None = None
164    enabled: int | None = None
165
166    @property
167    def start_time(self) -> datetime.time | None:
168        return (
169            datetime.time(hour=self.start_hour, minute=self.start_minute)
170            if self.start_hour is not None and self.start_minute is not None
171            else None
172        )
173
174    @property
175    def end_time(self) -> datetime.time | None:
176        return (
177            datetime.time(hour=self.end_hour, minute=self.end_minute)
178            if self.end_hour is not None and self.end_minute is not None
179            else None
180        )
181
182    def as_list(self) -> list:
183        return [self.start_hour, self.start_minute, self.end_hour, self.end_minute]
184
185    def __repr__(self) -> str:
186        return _attr_repr(self)
RoborockBaseTimer( start_hour: int | None = None, start_minute: int | None = None, end_hour: int | None = None, end_minute: int | None = None, enabled: int | None = None)
start_hour: int | None = None
start_minute: int | None = None
end_hour: int | None = None
end_minute: int | None = None
enabled: int | None = None
start_time: datetime.time | None
166    @property
167    def start_time(self) -> datetime.time | None:
168        return (
169            datetime.time(hour=self.start_hour, minute=self.start_minute)
170            if self.start_hour is not None and self.start_minute is not None
171            else None
172        )
end_time: datetime.time | None
174    @property
175    def end_time(self) -> datetime.time | None:
176        return (
177            datetime.time(hour=self.end_hour, minute=self.end_minute)
178            if self.end_hour is not None and self.end_minute is not None
179            else None
180        )
def as_list(self) -> list:
182    def as_list(self) -> list:
183        return [self.start_hour, self.start_minute, self.end_hour, self.end_minute]
@dataclass
class Reference(RoborockBase):
189@dataclass
190class Reference(RoborockBase):
191    r: str | None = None
192    a: str | None = None
193    m: str | None = None
194    l: str | None = None
Reference( r: str | None = None, a: str | None = None, m: str | None = None, l: str | None = None)
r: str | None = None
a: str | None = None
m: str | None = None
l: str | None = None
@dataclass
class RRiot(RoborockBase):
197@dataclass
198class RRiot(RoborockBase):
199    u: str
200    s: str
201    h: str
202    k: str
203    r: Reference
RRiot( u: str, s: str, h: str, k: str, r: Reference)
u: str
s: str
h: str
k: str
@dataclass
class UserData(RoborockBase):
206@dataclass
207class UserData(RoborockBase):
208    rriot: RRiot
209    uid: int | None = None
210    tokentype: str | None = None
211    token: str | None = None
212    rruid: str | None = None
213    region: str | None = None
214    countrycode: str | None = None
215    country: str | None = None
216    nickname: str | None = None
217    tuya_device_state: int | None = None
218    avatarurl: str | None = None
UserData( rriot: RRiot, uid: int | None = None, tokentype: str | None = None, token: str | None = None, rruid: str | None = None, region: str | None = None, countrycode: str | None = None, country: str | None = None, nickname: str | None = None, tuya_device_state: int | None = None, avatarurl: str | None = None)
rriot: RRiot
uid: int | None = None
tokentype: str | None = None
token: str | None = None
rruid: str | None = None
region: str | None = None
countrycode: str | None = None
country: str | None = None
nickname: str | None = None
tuya_device_state: int | None = None
avatarurl: str | None = None
@dataclass
class HomeDataProductSchema(RoborockBase):
221@dataclass
222class HomeDataProductSchema(RoborockBase):
223    id: Any | None = None
224    name: Any | None = None
225    code: Any | None = None
226    mode: Any | None = None
227    type: Any | None = None
228    product_property: Any | None = None
229    property: Any | None = None
230    desc: Any | None = None
HomeDataProductSchema( id: typing.Any | None = None, name: typing.Any | None = None, code: typing.Any | None = None, mode: typing.Any | None = None, type: typing.Any | None = None, product_property: typing.Any | None = None, property: typing.Any | None = None, desc: typing.Any | None = None)
id: typing.Any | None = None
name: typing.Any | None = None
code: typing.Any | None = None
mode: typing.Any | None = None
type: typing.Any | None = None
product_property: typing.Any | None = None
property: typing.Any | None = None
desc: typing.Any | None = None
@dataclass
class HomeDataProduct(RoborockBase):
233@dataclass
234class HomeDataProduct(RoborockBase):
235    id: str
236    name: str
237    model: str
238    category: RoborockCategory
239    code: str | None = None
240    icon_url: str | None = None
241    attribute: Any | None = None
242    capability: int | None = None
243    schema: list[HomeDataProductSchema] | None = None
244
245    @property
246    def product_nickname(self) -> RoborockProductNickname:
247        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
248
249    def summary_info(self) -> str:
250        """Return a string with key product information for logging purposes."""
251        return f"{self.name} (model={self.model}, category={self.category})"
252
253    @cached_property
254    def supported_schema_codes(self) -> set[str]:
255        """Return a set of fields that are supported by the device."""
256        if self.schema is None:
257            return set()
258        return {schema.code for schema in self.schema if schema.code is not None}
HomeDataProduct( id: str, name: str, model: str, category: roborock.data.code_mappings.RoborockCategory, code: str | None = None, icon_url: str | None = None, attribute: typing.Any | None = None, capability: int | None = None, schema: list[HomeDataProductSchema] | None = None)
id: str
name: str
model: str
code: str | None = None
icon_url: str | None = None
attribute: typing.Any | None = None
capability: int | None = None
schema: list[HomeDataProductSchema] | None = None
245    @property
246    def product_nickname(self) -> RoborockProductNickname:
247        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
def summary_info(self) -> str:
249    def summary_info(self) -> str:
250        """Return a string with key product information for logging purposes."""
251        return f"{self.name} (model={self.model}, category={self.category})"

Return a string with key product information for logging purposes.

supported_schema_codes: set[str]
253    @cached_property
254    def supported_schema_codes(self) -> set[str]:
255        """Return a set of fields that are supported by the device."""
256        if self.schema is None:
257            return set()
258        return {schema.code for schema in self.schema if schema.code is not None}

Return a set of fields that are supported by the device.

@dataclass
class HomeDataDevice(RoborockBase):
261@dataclass
262class HomeDataDevice(RoborockBase):
263    duid: str
264    name: str
265    local_key: str
266    product_id: str
267    fv: str | None = None
268    attribute: Any | None = None
269    active_time: int | None = None
270    runtime_env: Any | None = None
271    time_zone_id: str | None = None
272    icon_url: str | None = None
273    lon: Any | None = None
274    lat: Any | None = None
275    share: Any | None = None
276    share_time: Any | None = None
277    online: bool | None = None
278    pv: str | None = None
279    room_id: Any | None = None
280    tuya_uuid: Any | None = None
281    tuya_migrated: bool | None = None
282    extra: Any | None = None
283    sn: str | None = None
284    feature_set: str | None = None
285    new_feature_set: str | None = None
286    device_status: dict | None = None
287    silent_ota_switch: bool | None = None
288    setting: Any | None = None
289    f: bool | None = None
290    create_time: int | None = None
291    cid: str | None = None
292    share_type: Any | None = None
293    share_expired_time: int | None = None
294
295    def summary_info(self) -> str:
296        """Return a string with key device information for logging purposes."""
297        return f"{self.name} (pv={self.pv}, fv={self.fv}, online={self.online})"
HomeDataDevice( duid: str, name: str, local_key: str, product_id: str, fv: str | None = None, attribute: typing.Any | None = None, active_time: int | None = None, runtime_env: typing.Any | None = None, time_zone_id: str | None = None, icon_url: str | None = None, lon: typing.Any | None = None, lat: typing.Any | None = None, share: typing.Any | None = None, share_time: typing.Any | None = None, online: bool | None = None, pv: str | None = None, room_id: typing.Any | None = None, tuya_uuid: typing.Any | None = None, tuya_migrated: bool | None = None, extra: typing.Any | None = None, sn: str | None = None, feature_set: str | None = None, new_feature_set: str | None = None, device_status: dict | None = None, silent_ota_switch: bool | None = None, setting: typing.Any | None = None, f: bool | None = None, create_time: int | None = None, cid: str | None = None, share_type: typing.Any | None = None, share_expired_time: int | None = None)
duid: str
name: str
local_key: str
product_id: str
fv: str | None = None
attribute: typing.Any | None = None
active_time: int | None = None
runtime_env: typing.Any | None = None
time_zone_id: str | None = None
icon_url: str | None = None
lon: typing.Any | None = None
lat: typing.Any | None = None
share: typing.Any | None = None
share_time: typing.Any | None = None
online: bool | None = None
pv: str | None = None
room_id: typing.Any | None = None
tuya_uuid: typing.Any | None = None
tuya_migrated: bool | None = None
extra: typing.Any | None = None
sn: str | None = None
feature_set: str | None = None
new_feature_set: str | None = None
device_status: dict | None = None
silent_ota_switch: bool | None = None
setting: typing.Any | None = None
f: bool | None = None
create_time: int | None = None
cid: str | None = None
share_type: typing.Any | None = None
share_expired_time: int | None = None
def summary_info(self) -> str:
295    def summary_info(self) -> str:
296        """Return a string with key device information for logging purposes."""
297        return f"{self.name} (pv={self.pv}, fv={self.fv}, online={self.online})"

Return a string with key device information for logging purposes.

@dataclass
class HomeDataRoom(RoborockBase):
300@dataclass
301class HomeDataRoom(RoborockBase):
302    id: int
303    name: str
304
305    @property
306    def iot_id(self) -> str:
307        """Return the room's ID as a string IOT ID."""
308        return str(self.id)
HomeDataRoom(id: int, name: str)
id: int
name: str
iot_id: str
305    @property
306    def iot_id(self) -> str:
307        """Return the room's ID as a string IOT ID."""
308        return str(self.id)

Return the room's ID as a string IOT ID.

@dataclass
class HomeDataScene(RoborockBase):
311@dataclass
312class HomeDataScene(RoborockBase):
313    id: int
314    name: str
HomeDataScene(id: int, name: str)
id: int
name: str
@dataclass
class HomeDataSchedule(RoborockBase):
317@dataclass
318class HomeDataSchedule(RoborockBase):
319    id: int
320    cron: str
321    repeated: bool
322    enabled: bool
323    param: dict | None = None
HomeDataSchedule( id: int, cron: str, repeated: bool, enabled: bool, param: dict | None = None)
id: int
cron: str
repeated: bool
enabled: bool
param: dict | None = None
@dataclass
class HomeData(RoborockBase):
326@dataclass
327class HomeData(RoborockBase):
328    id: int
329    name: str
330    products: list[HomeDataProduct] = field(default_factory=lambda: [])
331    devices: list[HomeDataDevice] = field(default_factory=lambda: [])
332    received_devices: list[HomeDataDevice] = field(default_factory=lambda: [])
333    lon: Any | None = None
334    lat: Any | None = None
335    geo_name: Any | None = None
336    rooms: list[HomeDataRoom] = field(default_factory=list)
337
338    def get_all_devices(self) -> list[HomeDataDevice]:
339        devices = []
340        if self.devices is not None:
341            devices += self.devices
342        if self.received_devices is not None:
343            devices += self.received_devices
344        return devices
345
346    @cached_property
347    def product_map(self) -> dict[str, HomeDataProduct]:
348        """Returns a dictionary of product IDs to HomeDataProduct objects."""
349        return {product.id: product for product in self.products}
350
351    @cached_property
352    def device_products(self) -> dict[str, tuple[HomeDataDevice, HomeDataProduct]]:
353        """Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects."""
354        product_map = self.product_map
355        return {
356            device.duid: (device, product)
357            for device in self.get_all_devices()
358            if (product := product_map.get(device.product_id)) is not None
359        }
360
361    @property
362    def rooms_map(self) -> dict[str, HomeDataRoom]:
363        """Returns a dictionary of Room iot_id to rooms"""
364        return {room.iot_id: room for room in self.rooms}
365
366    @property
367    def rooms_name_map(self) -> dict[str, str]:
368        """Returns a dictionary of Room iot_id to room names."""
369        return {room.iot_id: room.name for room in self.rooms}
HomeData( id: int, name: str, products: list[HomeDataProduct] = <factory>, devices: list[HomeDataDevice] = <factory>, received_devices: list[HomeDataDevice] = <factory>, lon: typing.Any | None = None, lat: typing.Any | None = None, geo_name: typing.Any | None = None, rooms: list[HomeDataRoom] = <factory>)
id: int
name: str
products: list[HomeDataProduct]
devices: list[HomeDataDevice]
received_devices: list[HomeDataDevice]
lon: typing.Any | None = None
lat: typing.Any | None = None
geo_name: typing.Any | None = None
rooms: list[HomeDataRoom]
def get_all_devices(self) -> list[HomeDataDevice]:
338    def get_all_devices(self) -> list[HomeDataDevice]:
339        devices = []
340        if self.devices is not None:
341            devices += self.devices
342        if self.received_devices is not None:
343            devices += self.received_devices
344        return devices
product_map: dict[str, HomeDataProduct]
346    @cached_property
347    def product_map(self) -> dict[str, HomeDataProduct]:
348        """Returns a dictionary of product IDs to HomeDataProduct objects."""
349        return {product.id: product for product in self.products}

Returns a dictionary of product IDs to HomeDataProduct objects.

device_products: dict[str, tuple[HomeDataDevice, HomeDataProduct]]
351    @cached_property
352    def device_products(self) -> dict[str, tuple[HomeDataDevice, HomeDataProduct]]:
353        """Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects."""
354        product_map = self.product_map
355        return {
356            device.duid: (device, product)
357            for device in self.get_all_devices()
358            if (product := product_map.get(device.product_id)) is not None
359        }

Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects.

rooms_map: dict[str, HomeDataRoom]
361    @property
362    def rooms_map(self) -> dict[str, HomeDataRoom]:
363        """Returns a dictionary of Room iot_id to rooms"""
364        return {room.iot_id: room for room in self.rooms}

Returns a dictionary of Room iot_id to rooms

rooms_name_map: dict[str, str]
366    @property
367    def rooms_name_map(self) -> dict[str, str]:
368        """Returns a dictionary of Room iot_id to room names."""
369        return {room.iot_id: room.name for room in self.rooms}

Returns a dictionary of Room iot_id to room names.

@dataclass
class LoginData(RoborockBase):
372@dataclass
373class LoginData(RoborockBase):
374    user_data: UserData
375    email: str
376    home_data: HomeData | None = None
LoginData( user_data: UserData, email: str, home_data: HomeData | None = None)
user_data: UserData
email: str
home_data: HomeData | None = None
@dataclass
class DeviceData(RoborockBase):
379@dataclass
380class DeviceData(RoborockBase):
381    device: HomeDataDevice
382    model: str
383    host: str | None = None
384
385    @property
386    def product_nickname(self) -> RoborockProductNickname:
387        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
388
389    def __repr__(self) -> str:
390        return _attr_repr(self)
DeviceData( device: HomeDataDevice, model: str, host: str | None = None)
device: HomeDataDevice
model: str
host: str | None = None
385    @property
386    def product_nickname(self) -> RoborockProductNickname:
387        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
@dataclass
class RoomMapping(RoborockBase):
393@dataclass
394class RoomMapping(RoborockBase):
395    segment_id: int
396    iot_id: str
RoomMapping(segment_id: int, iot_id: str)
segment_id: int
iot_id: str
@dataclass
class NamedRoomMapping(RoomMapping):
399@dataclass
400class NamedRoomMapping(RoomMapping):
401    """Dataclass representing a mapping of a room segment to a name.
402
403    The name information is not provided by the device directly, but is provided
404    from the HomeData based on the iot_id from the room.
405    """
406
407    @property
408    def name(self) -> str:
409        """The human-readable name of the room, or a default name if not available."""
410        return self.raw_name or f"Room {self.segment_id}"
411
412    raw_name: str | None = None
413    """The raw name of the room, as provided by the device."""

Dataclass representing a mapping of a room segment to a name.

The name information is not provided by the device directly, but is provided from the HomeData based on the iot_id from the room.

NamedRoomMapping(segment_id: int, iot_id: str, raw_name: str | None = None)
name: str
407    @property
408    def name(self) -> str:
409        """The human-readable name of the room, or a default name if not available."""
410        return self.raw_name or f"Room {self.segment_id}"

The human-readable name of the room, or a default name if not available.

raw_name: str | None = None

The raw name of the room, as provided by the device.

@dataclass
class CombinedMapInfo(RoborockBase):
416@dataclass
417class CombinedMapInfo(RoborockBase):
418    """Data structure for caching home information.
419
420    This is not provided directly by the API, but is a combination of map data
421    and room data to provide a more useful structure.
422    """
423
424    map_flag: int
425    """The map identifier."""
426
427    name: str
428    """The name of the map from MultiMapsListMapInfo."""
429
430    rooms: list[NamedRoomMapping]
431    """The list of rooms in the map."""
432
433    @property
434    def rooms_map(self) -> dict[int, NamedRoomMapping]:
435        """Returns a mapping of segment_id to NamedRoomMapping."""
436        return {room.segment_id: room for room in self.rooms}

Data structure for caching home information.

This is not provided directly by the API, but is a combination of map data and room data to provide a more useful structure.

CombinedMapInfo( map_flag: int, name: str, rooms: list[NamedRoomMapping])
map_flag: int

The map identifier.

name: str

The name of the map from MultiMapsListMapInfo.

rooms: list[NamedRoomMapping]

The list of rooms in the map.

rooms_map: dict[int, NamedRoomMapping]
433    @property
434    def rooms_map(self) -> dict[int, NamedRoomMapping]:
435        """Returns a mapping of segment_id to NamedRoomMapping."""
436        return {room.segment_id: room for room in self.rooms}

Returns a mapping of segment_id to NamedRoomMapping.

@dataclass
class BroadcastMessage(RoborockBase):
439@dataclass
440class BroadcastMessage(RoborockBase):
441    duid: str
442    ip: str
443    version: bytes
BroadcastMessage(duid: str, ip: str, version: bytes)
duid: str
ip: str
version: bytes
class ServerTimer(typing.NamedTuple):
446class ServerTimer(NamedTuple):
447    id: str
448    status: str
449    dontknow: int

ServerTimer(id, status, dontknow)

ServerTimer(id: str, status: str, dontknow: int)

Create new instance of ServerTimer(id, status, dontknow)

id: str

Alias for field number 0

status: str

Alias for field number 1

dontknow: int

Alias for field number 2

@dataclass
class RoborockProductStateValue(RoborockBase):
452@dataclass
453class RoborockProductStateValue(RoborockBase):
454    value: list
455    desc: dict
RoborockProductStateValue(value: list, desc: dict)
value: list
desc: dict
@dataclass
class RoborockProductState(RoborockBase):
458@dataclass
459class RoborockProductState(RoborockBase):
460    dps: int
461    desc: dict
462    value: list[RoborockProductStateValue]
RoborockProductState( dps: int, desc: dict, value: list[RoborockProductStateValue])
dps: int
desc: dict
@dataclass
class RoborockProductSpec(RoborockBase):
465@dataclass
466class RoborockProductSpec(RoborockBase):
467    state: RoborockProductState
468    battery: dict | None = None
469    dry_countdown: dict | None = None
470    extra: dict | None = None
471    offpeak: dict | None = None
472    countdown: dict | None = None
473    mode: dict | None = None
474    ota_nfo: dict | None = None
475    pause: dict | None = None
476    program: dict | None = None
477    shutdown: dict | None = None
478    washing_left: dict | None = None
RoborockProductSpec( state: RoborockProductState, battery: dict | None = None, dry_countdown: dict | None = None, extra: dict | None = None, offpeak: dict | None = None, countdown: dict | None = None, mode: dict | None = None, ota_nfo: dict | None = None, pause: dict | None = None, program: dict | None = None, shutdown: dict | None = None, washing_left: dict | None = None)
battery: dict | None = None
dry_countdown: dict | None = None
extra: dict | None = None
offpeak: dict | None = None
countdown: dict | None = None
mode: dict | None = None
ota_nfo: dict | None = None
pause: dict | None = None
program: dict | None = None
shutdown: dict | None = None
washing_left: dict | None = None
@dataclass
class RoborockProduct(RoborockBase):
481@dataclass
482class RoborockProduct(RoborockBase):
483    id: int | None = None
484    name: str | None = None
485    model: str | None = None
486    packagename: str | None = None
487    ssid: str | None = None
488    picurl: str | None = None
489    cardpicurl: str | None = None
490    mediumCardpicurl: str | None = None
491    resetwifipicurl: str | None = None
492    configPicUrl: str | None = None
493    pluginPicUrl: str | None = None
494    resetwifitext: dict | None = None
495    tuyaid: str | None = None
496    status: int | None = None
497    rriotid: str | None = None
498    pictures: list | None = None
499    ncMode: str | None = None
500    scope: str | None = None
501    product_tags: list | None = None
502    agreements: list | None = None
503    cardspec: str | None = None
504    plugin_pic_url: str | None = None
505
506    @property
507    def product_nickname(self) -> RoborockProductNickname | None:
508        if self.cardspec:
509            return RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data"))
510        return None
511
512    def __repr__(self) -> str:
513        return _attr_repr(self)
RoborockProduct( id: int | None = None, name: str | None = None, model: str | None = None, packagename: str | None = None, ssid: str | None = None, picurl: str | None = None, cardpicurl: str | None = None, mediumCardpicurl: str | None = None, resetwifipicurl: str | None = None, configPicUrl: str | None = None, pluginPicUrl: str | None = None, resetwifitext: dict | None = None, tuyaid: str | None = None, status: int | None = None, rriotid: str | None = None, pictures: list | None = None, ncMode: str | None = None, scope: str | None = None, product_tags: list | None = None, agreements: list | None = None, cardspec: str | None = None, plugin_pic_url: str | None = None)
id: int | None = None
name: str | None = None
model: str | None = None
packagename: str | None = None
ssid: str | None = None
picurl: str | None = None
cardpicurl: str | None = None
mediumCardpicurl: str | None = None
resetwifipicurl: str | None = None
configPicUrl: str | None = None
pluginPicUrl: str | None = None
resetwifitext: dict | None = None
tuyaid: str | None = None
status: int | None = None
rriotid: str | None = None
pictures: list | None = None
ncMode: str | None = None
scope: str | None = None
product_tags: list | None = None
agreements: list | None = None
cardspec: str | None = None
plugin_pic_url: str | None = None
product_nickname: roborock.data.code_mappings.RoborockProductNickname | None
506    @property
507    def product_nickname(self) -> RoborockProductNickname | None:
508        if self.cardspec:
509            return RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data"))
510        return None
@dataclass
class RoborockProductCategory(RoborockBase):
516@dataclass
517class RoborockProductCategory(RoborockBase):
518    id: int
519    display_name: str
520    icon_url: str
RoborockProductCategory(id: int, display_name: str, icon_url: str)
id: int
display_name: str
icon_url: str
@dataclass
class RoborockCategoryDetail(RoborockBase):
523@dataclass
524class RoborockCategoryDetail(RoborockBase):
525    category: RoborockProductCategory
526    product_list: list[RoborockProduct]
RoborockCategoryDetail( category: RoborockProductCategory, product_list: list[RoborockProduct])
product_list: list[RoborockProduct]
@dataclass
class ProductResponse(RoborockBase):
529@dataclass
530class ProductResponse(RoborockBase):
531    category_detail_list: list[RoborockCategoryDetail]
ProductResponse( category_detail_list: list[RoborockCategoryDetail])
category_detail_list: list[RoborockCategoryDetail]