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        result: dict[str, Any] = {}
 95        for orig_key, value in data.items():
 96            key = _decamelize(orig_key)
 97            if (field_type := 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            if value == "None" or value is None:
108                result[key] = None
109                continue
110            if isinstance(field_type, types.UnionType):
111                for subtype in get_args(field_type):
112                    if subtype is types.NoneType:
113                        continue
114                    try:
115                        result[key] = RoborockBase._convert_to_class_obj(subtype, value)
116                        break
117                    except Exception:
118                        _LOGGER.exception(f"Failed to convert {key} with value {value} to type {subtype}")
119                        continue
120            else:
121                try:
122                    result[key] = RoborockBase._convert_to_class_obj(field_type, value)
123                except Exception:
124                    _LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}")
125                    continue
126
127        return cls(**result)
128
129    def as_dict(self) -> dict:
130        return asdict(
131            self,
132            dict_factory=lambda _fields: {
133                _camelize(key): value.value if isinstance(value, Enum) else value
134                for (key, value) in _fields
135                if value is not None
136            },
137        )
138
139
140@dataclass
141class RoborockBaseTimer(RoborockBase):
142    start_hour: int | None = None
143    start_minute: int | None = None
144    end_hour: int | None = None
145    end_minute: int | None = None
146    enabled: int | None = None
147
148    @property
149    def start_time(self) -> datetime.time | None:
150        return (
151            datetime.time(hour=self.start_hour, minute=self.start_minute)
152            if self.start_hour is not None and self.start_minute is not None
153            else None
154        )
155
156    @property
157    def end_time(self) -> datetime.time | None:
158        return (
159            datetime.time(hour=self.end_hour, minute=self.end_minute)
160            if self.end_hour is not None and self.end_minute is not None
161            else None
162        )
163
164    def as_list(self) -> list:
165        return [self.start_hour, self.start_minute, self.end_hour, self.end_minute]
166
167    def __repr__(self) -> str:
168        return _attr_repr(self)
169
170
171@dataclass
172class Reference(RoborockBase):
173    r: str | None = None
174    a: str | None = None
175    m: str | None = None
176    l: str | None = None
177
178
179@dataclass
180class RRiot(RoborockBase):
181    u: str
182    s: str
183    h: str
184    k: str
185    r: Reference
186
187
188@dataclass
189class UserData(RoborockBase):
190    rriot: RRiot
191    uid: int | None = None
192    tokentype: str | None = None
193    token: str | None = None
194    rruid: str | None = None
195    region: str | None = None
196    countrycode: str | None = None
197    country: str | None = None
198    nickname: str | None = None
199    tuya_device_state: int | None = None
200    avatarurl: str | None = None
201
202
203@dataclass
204class HomeDataProductSchema(RoborockBase):
205    id: Any | None = None
206    name: Any | None = None
207    code: Any | None = None
208    mode: Any | None = None
209    type: Any | None = None
210    product_property: Any | None = None
211    property: Any | None = None
212    desc: Any | None = None
213
214
215@dataclass
216class HomeDataProduct(RoborockBase):
217    id: str
218    name: str
219    model: str
220    category: RoborockCategory
221    code: str | None = None
222    icon_url: str | None = None
223    attribute: Any | None = None
224    capability: int | None = None
225    schema: list[HomeDataProductSchema] | None = None
226
227    @property
228    def product_nickname(self) -> RoborockProductNickname:
229        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
230
231    def summary_info(self) -> str:
232        """Return a string with key product information for logging purposes."""
233        return f"{self.name} (model={self.model}, category={self.category})"
234
235    @cached_property
236    def supported_schema_codes(self) -> set[str]:
237        """Return a set of fields that are supported by the device."""
238        if self.schema is None:
239            return set()
240        return {schema.code for schema in self.schema if schema.code is not None}
241
242
243@dataclass
244class HomeDataDevice(RoborockBase):
245    duid: str
246    name: str
247    local_key: str
248    product_id: str
249    fv: str | None = None
250    attribute: Any | None = None
251    active_time: int | None = None
252    runtime_env: Any | None = None
253    time_zone_id: str | None = None
254    icon_url: str | None = None
255    lon: Any | None = None
256    lat: Any | None = None
257    share: Any | None = None
258    share_time: Any | None = None
259    online: bool | None = None
260    pv: str | None = None
261    room_id: Any | None = None
262    tuya_uuid: Any | None = None
263    tuya_migrated: bool | None = None
264    extra: Any | None = None
265    sn: str | None = None
266    feature_set: str | None = None
267    new_feature_set: str | None = None
268    device_status: dict | None = None
269    silent_ota_switch: bool | None = None
270    setting: Any | None = None
271    f: bool | None = None
272    create_time: int | None = None
273    cid: str | None = None
274    share_type: Any | None = None
275    share_expired_time: int | None = None
276
277    def summary_info(self) -> str:
278        """Return a string with key device information for logging purposes."""
279        return f"{self.name} (pv={self.pv}, fv={self.fv}, online={self.online})"
280
281
282@dataclass
283class HomeDataRoom(RoborockBase):
284    id: int
285    name: str
286
287
288@dataclass
289class HomeDataScene(RoborockBase):
290    id: int
291    name: str
292
293
294@dataclass
295class HomeDataSchedule(RoborockBase):
296    id: int
297    cron: str
298    repeated: bool
299    enabled: bool
300    param: dict | None = None
301
302
303@dataclass
304class HomeData(RoborockBase):
305    id: int
306    name: str
307    products: list[HomeDataProduct] = field(default_factory=lambda: [])
308    devices: list[HomeDataDevice] = field(default_factory=lambda: [])
309    received_devices: list[HomeDataDevice] = field(default_factory=lambda: [])
310    lon: Any | None = None
311    lat: Any | None = None
312    geo_name: Any | None = None
313    rooms: list[HomeDataRoom] = field(default_factory=list)
314
315    def get_all_devices(self) -> list[HomeDataDevice]:
316        devices = []
317        if self.devices is not None:
318            devices += self.devices
319        if self.received_devices is not None:
320            devices += self.received_devices
321        return devices
322
323    @cached_property
324    def product_map(self) -> dict[str, HomeDataProduct]:
325        """Returns a dictionary of product IDs to HomeDataProduct objects."""
326        return {product.id: product for product in self.products}
327
328    @cached_property
329    def device_products(self) -> dict[str, tuple[HomeDataDevice, HomeDataProduct]]:
330        """Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects."""
331        product_map = self.product_map
332        return {
333            device.duid: (device, product)
334            for device in self.get_all_devices()
335            if (product := product_map.get(device.product_id)) is not None
336        }
337
338
339@dataclass
340class LoginData(RoborockBase):
341    user_data: UserData
342    email: str
343    home_data: HomeData | None = None
344
345
346@dataclass
347class DeviceData(RoborockBase):
348    device: HomeDataDevice
349    model: str
350    host: str | None = None
351
352    @property
353    def product_nickname(self) -> RoborockProductNickname:
354        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
355
356    def __repr__(self) -> str:
357        return _attr_repr(self)
358
359
360@dataclass
361class RoomMapping(RoborockBase):
362    segment_id: int
363    iot_id: str
364
365
366@dataclass
367class NamedRoomMapping(RoomMapping):
368    """Dataclass representing a mapping of a room segment to a name.
369
370    The name information is not provided by the device directly, but is provided
371    from the HomeData based on the iot_id from the room.
372    """
373
374    name: str
375    """The human-readable name of the room, if available."""
376
377
378@dataclass
379class CombinedMapInfo(RoborockBase):
380    """Data structure for caching home information.
381
382    This is not provided directly by the API, but is a combination of map data
383    and room data to provide a more useful structure.
384    """
385
386    map_flag: int
387    """The map identifier."""
388
389    name: str
390    """The name of the map from MultiMapsListMapInfo."""
391
392    rooms: list[NamedRoomMapping]
393    """The list of rooms in the map."""
394
395
396@dataclass
397class BroadcastMessage(RoborockBase):
398    duid: str
399    ip: str
400    version: bytes
401
402
403class ServerTimer(NamedTuple):
404    id: str
405    status: str
406    dontknow: int
407
408
409@dataclass
410class RoborockProductStateValue(RoborockBase):
411    value: list
412    desc: dict
413
414
415@dataclass
416class RoborockProductState(RoborockBase):
417    dps: int
418    desc: dict
419    value: list[RoborockProductStateValue]
420
421
422@dataclass
423class RoborockProductSpec(RoborockBase):
424    state: RoborockProductState
425    battery: dict | None = None
426    dry_countdown: dict | None = None
427    extra: dict | None = None
428    offpeak: dict | None = None
429    countdown: dict | None = None
430    mode: dict | None = None
431    ota_nfo: dict | None = None
432    pause: dict | None = None
433    program: dict | None = None
434    shutdown: dict | None = None
435    washing_left: dict | None = None
436
437
438@dataclass
439class RoborockProduct(RoborockBase):
440    id: int | None = None
441    name: str | None = None
442    model: str | None = None
443    packagename: str | None = None
444    ssid: str | None = None
445    picurl: str | None = None
446    cardpicurl: str | None = None
447    mediumCardpicurl: str | None = None
448    resetwifipicurl: str | None = None
449    configPicUrl: str | None = None
450    pluginPicUrl: str | None = None
451    resetwifitext: dict | None = None
452    tuyaid: str | None = None
453    status: int | None = None
454    rriotid: str | None = None
455    pictures: list | None = None
456    ncMode: str | None = None
457    scope: str | None = None
458    product_tags: list | None = None
459    agreements: list | None = None
460    cardspec: str | None = None
461    plugin_pic_url: str | None = None
462
463    @property
464    def product_nickname(self) -> RoborockProductNickname | None:
465        if self.cardspec:
466            return RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data"))
467        return None
468
469    def __repr__(self) -> str:
470        return _attr_repr(self)
471
472
473@dataclass
474class RoborockProductCategory(RoborockBase):
475    id: int
476    display_name: str
477    icon_url: str
478
479
480@dataclass
481class RoborockCategoryDetail(RoborockBase):
482    category: RoborockProductCategory
483    product_list: list[RoborockProduct]
484
485
486@dataclass
487class ProductResponse(RoborockBase):
488    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        result: dict[str, Any] = {}
 96        for orig_key, value in data.items():
 97            key = _decamelize(orig_key)
 98            if (field_type := 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            if value == "None" or value is None:
109                result[key] = None
110                continue
111            if isinstance(field_type, types.UnionType):
112                for subtype in get_args(field_type):
113                    if subtype is types.NoneType:
114                        continue
115                    try:
116                        result[key] = RoborockBase._convert_to_class_obj(subtype, value)
117                        break
118                    except Exception:
119                        _LOGGER.exception(f"Failed to convert {key} with value {value} to type {subtype}")
120                        continue
121            else:
122                try:
123                    result[key] = RoborockBase._convert_to_class_obj(field_type, value)
124                except Exception:
125                    _LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}")
126                    continue
127
128        return cls(**result)
129
130    def as_dict(self) -> dict:
131        return asdict(
132            self,
133            dict_factory=lambda _fields: {
134                _camelize(key): value.value if isinstance(value, Enum) else value
135                for (key, value) in _fields
136                if value is not None
137            },
138        )

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        result: dict[str, Any] = {}
 96        for orig_key, value in data.items():
 97            key = _decamelize(orig_key)
 98            if (field_type := 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            if value == "None" or value is None:
109                result[key] = None
110                continue
111            if isinstance(field_type, types.UnionType):
112                for subtype in get_args(field_type):
113                    if subtype is types.NoneType:
114                        continue
115                    try:
116                        result[key] = RoborockBase._convert_to_class_obj(subtype, value)
117                        break
118                    except Exception:
119                        _LOGGER.exception(f"Failed to convert {key} with value {value} to type {subtype}")
120                        continue
121            else:
122                try:
123                    result[key] = RoborockBase._convert_to_class_obj(field_type, value)
124                except Exception:
125                    _LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}")
126                    continue
127
128        return cls(**result)

Create an instance of the class from a dictionary.

def as_dict(self) -> dict:
130    def as_dict(self) -> dict:
131        return asdict(
132            self,
133            dict_factory=lambda _fields: {
134                _camelize(key): value.value if isinstance(value, Enum) else value
135                for (key, value) in _fields
136                if value is not None
137            },
138        )
@dataclass
class RoborockBaseTimer(RoborockBase):
141@dataclass
142class RoborockBaseTimer(RoborockBase):
143    start_hour: int | None = None
144    start_minute: int | None = None
145    end_hour: int | None = None
146    end_minute: int | None = None
147    enabled: int | None = None
148
149    @property
150    def start_time(self) -> datetime.time | None:
151        return (
152            datetime.time(hour=self.start_hour, minute=self.start_minute)
153            if self.start_hour is not None and self.start_minute is not None
154            else None
155        )
156
157    @property
158    def end_time(self) -> datetime.time | None:
159        return (
160            datetime.time(hour=self.end_hour, minute=self.end_minute)
161            if self.end_hour is not None and self.end_minute is not None
162            else None
163        )
164
165    def as_list(self) -> list:
166        return [self.start_hour, self.start_minute, self.end_hour, self.end_minute]
167
168    def __repr__(self) -> str:
169        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
149    @property
150    def start_time(self) -> datetime.time | None:
151        return (
152            datetime.time(hour=self.start_hour, minute=self.start_minute)
153            if self.start_hour is not None and self.start_minute is not None
154            else None
155        )
end_time: datetime.time | None
157    @property
158    def end_time(self) -> datetime.time | None:
159        return (
160            datetime.time(hour=self.end_hour, minute=self.end_minute)
161            if self.end_hour is not None and self.end_minute is not None
162            else None
163        )
def as_list(self) -> list:
165    def as_list(self) -> list:
166        return [self.start_hour, self.start_minute, self.end_hour, self.end_minute]
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class Reference(RoborockBase):
172@dataclass
173class Reference(RoborockBase):
174    r: str | None = None
175    a: str | None = None
176    m: str | None = None
177    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
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class RRiot(RoborockBase):
180@dataclass
181class RRiot(RoborockBase):
182    u: str
183    s: str
184    h: str
185    k: str
186    r: Reference
RRiot( u: str, s: str, h: str, k: str, r: Reference)
u: str
s: str
h: str
k: str
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class UserData(RoborockBase):
189@dataclass
190class UserData(RoborockBase):
191    rriot: RRiot
192    uid: int | None = None
193    tokentype: str | None = None
194    token: str | None = None
195    rruid: str | None = None
196    region: str | None = None
197    countrycode: str | None = None
198    country: str | None = None
199    nickname: str | None = None
200    tuya_device_state: int | None = None
201    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
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class HomeDataProductSchema(RoborockBase):
204@dataclass
205class HomeDataProductSchema(RoborockBase):
206    id: Any | None = None
207    name: Any | None = None
208    code: Any | None = None
209    mode: Any | None = None
210    type: Any | None = None
211    product_property: Any | None = None
212    property: Any | None = None
213    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
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class HomeDataProduct(RoborockBase):
216@dataclass
217class HomeDataProduct(RoborockBase):
218    id: str
219    name: str
220    model: str
221    category: RoborockCategory
222    code: str | None = None
223    icon_url: str | None = None
224    attribute: Any | None = None
225    capability: int | None = None
226    schema: list[HomeDataProductSchema] | None = None
227
228    @property
229    def product_nickname(self) -> RoborockProductNickname:
230        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
231
232    def summary_info(self) -> str:
233        """Return a string with key product information for logging purposes."""
234        return f"{self.name} (model={self.model}, category={self.category})"
235
236    @cached_property
237    def supported_schema_codes(self) -> set[str]:
238        """Return a set of fields that are supported by the device."""
239        if self.schema is None:
240            return set()
241        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
228    @property
229    def product_nickname(self) -> RoborockProductNickname:
230        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
def summary_info(self) -> str:
232    def summary_info(self) -> str:
233        """Return a string with key product information for logging purposes."""
234        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]
236    @cached_property
237    def supported_schema_codes(self) -> set[str]:
238        """Return a set of fields that are supported by the device."""
239        if self.schema is None:
240            return set()
241        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.

Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class HomeDataDevice(RoborockBase):
244@dataclass
245class HomeDataDevice(RoborockBase):
246    duid: str
247    name: str
248    local_key: str
249    product_id: str
250    fv: str | None = None
251    attribute: Any | None = None
252    active_time: int | None = None
253    runtime_env: Any | None = None
254    time_zone_id: str | None = None
255    icon_url: str | None = None
256    lon: Any | None = None
257    lat: Any | None = None
258    share: Any | None = None
259    share_time: Any | None = None
260    online: bool | None = None
261    pv: str | None = None
262    room_id: Any | None = None
263    tuya_uuid: Any | None = None
264    tuya_migrated: bool | None = None
265    extra: Any | None = None
266    sn: str | None = None
267    feature_set: str | None = None
268    new_feature_set: str | None = None
269    device_status: dict | None = None
270    silent_ota_switch: bool | None = None
271    setting: Any | None = None
272    f: bool | None = None
273    create_time: int | None = None
274    cid: str | None = None
275    share_type: Any | None = None
276    share_expired_time: int | None = None
277
278    def summary_info(self) -> str:
279        """Return a string with key device information for logging purposes."""
280        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:
278    def summary_info(self) -> str:
279        """Return a string with key device information for logging purposes."""
280        return f"{self.name} (pv={self.pv}, fv={self.fv}, online={self.online})"

Return a string with key device information for logging purposes.

Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class HomeDataRoom(RoborockBase):
283@dataclass
284class HomeDataRoom(RoborockBase):
285    id: int
286    name: str
HomeDataRoom(id: int, name: str)
id: int
name: str
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class HomeDataScene(RoborockBase):
289@dataclass
290class HomeDataScene(RoborockBase):
291    id: int
292    name: str
HomeDataScene(id: int, name: str)
id: int
name: str
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class HomeDataSchedule(RoborockBase):
295@dataclass
296class HomeDataSchedule(RoborockBase):
297    id: int
298    cron: str
299    repeated: bool
300    enabled: bool
301    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
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class HomeData(RoborockBase):
304@dataclass
305class HomeData(RoborockBase):
306    id: int
307    name: str
308    products: list[HomeDataProduct] = field(default_factory=lambda: [])
309    devices: list[HomeDataDevice] = field(default_factory=lambda: [])
310    received_devices: list[HomeDataDevice] = field(default_factory=lambda: [])
311    lon: Any | None = None
312    lat: Any | None = None
313    geo_name: Any | None = None
314    rooms: list[HomeDataRoom] = field(default_factory=list)
315
316    def get_all_devices(self) -> list[HomeDataDevice]:
317        devices = []
318        if self.devices is not None:
319            devices += self.devices
320        if self.received_devices is not None:
321            devices += self.received_devices
322        return devices
323
324    @cached_property
325    def product_map(self) -> dict[str, HomeDataProduct]:
326        """Returns a dictionary of product IDs to HomeDataProduct objects."""
327        return {product.id: product for product in self.products}
328
329    @cached_property
330    def device_products(self) -> dict[str, tuple[HomeDataDevice, HomeDataProduct]]:
331        """Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects."""
332        product_map = self.product_map
333        return {
334            device.duid: (device, product)
335            for device in self.get_all_devices()
336            if (product := product_map.get(device.product_id)) is not None
337        }
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]:
316    def get_all_devices(self) -> list[HomeDataDevice]:
317        devices = []
318        if self.devices is not None:
319            devices += self.devices
320        if self.received_devices is not None:
321            devices += self.received_devices
322        return devices
product_map: dict[str, HomeDataProduct]
324    @cached_property
325    def product_map(self) -> dict[str, HomeDataProduct]:
326        """Returns a dictionary of product IDs to HomeDataProduct objects."""
327        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]]
329    @cached_property
330    def device_products(self) -> dict[str, tuple[HomeDataDevice, HomeDataProduct]]:
331        """Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects."""
332        product_map = self.product_map
333        return {
334            device.duid: (device, product)
335            for device in self.get_all_devices()
336            if (product := product_map.get(device.product_id)) is not None
337        }

Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects.

Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class LoginData(RoborockBase):
340@dataclass
341class LoginData(RoborockBase):
342    user_data: UserData
343    email: str
344    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
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class DeviceData(RoborockBase):
347@dataclass
348class DeviceData(RoborockBase):
349    device: HomeDataDevice
350    model: str
351    host: str | None = None
352
353    @property
354    def product_nickname(self) -> RoborockProductNickname:
355        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
356
357    def __repr__(self) -> str:
358        return _attr_repr(self)
DeviceData( device: HomeDataDevice, model: str, host: str | None = None)
device: HomeDataDevice
model: str
host: str | None = None
353    @property
354    def product_nickname(self) -> RoborockProductNickname:
355        return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class RoomMapping(RoborockBase):
361@dataclass
362class RoomMapping(RoborockBase):
363    segment_id: int
364    iot_id: str
RoomMapping(segment_id: int, iot_id: str)
segment_id: int
iot_id: str
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class NamedRoomMapping(RoomMapping):
367@dataclass
368class NamedRoomMapping(RoomMapping):
369    """Dataclass representing a mapping of a room segment to a name.
370
371    The name information is not provided by the device directly, but is provided
372    from the HomeData based on the iot_id from the room.
373    """
374
375    name: str
376    """The human-readable name of the room, if available."""

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, name: str)
name: str

The human-readable name of the room, if available.

@dataclass
class CombinedMapInfo(RoborockBase):
379@dataclass
380class CombinedMapInfo(RoborockBase):
381    """Data structure for caching home information.
382
383    This is not provided directly by the API, but is a combination of map data
384    and room data to provide a more useful structure.
385    """
386
387    map_flag: int
388    """The map identifier."""
389
390    name: str
391    """The name of the map from MultiMapsListMapInfo."""
392
393    rooms: list[NamedRoomMapping]
394    """The list of rooms in the map."""

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.

Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class BroadcastMessage(RoborockBase):
397@dataclass
398class BroadcastMessage(RoborockBase):
399    duid: str
400    ip: str
401    version: bytes
BroadcastMessage(duid: str, ip: str, version: bytes)
duid: str
ip: str
version: bytes
Inherited Members
RoborockBase
from_dict
as_dict
class ServerTimer(typing.NamedTuple):
404class ServerTimer(NamedTuple):
405    id: str
406    status: str
407    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):
410@dataclass
411class RoborockProductStateValue(RoborockBase):
412    value: list
413    desc: dict
RoborockProductStateValue(value: list, desc: dict)
value: list
desc: dict
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class RoborockProductState(RoborockBase):
416@dataclass
417class RoborockProductState(RoborockBase):
418    dps: int
419    desc: dict
420    value: list[RoborockProductStateValue]
RoborockProductState( dps: int, desc: dict, value: list[RoborockProductStateValue])
dps: int
desc: dict
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class RoborockProductSpec(RoborockBase):
423@dataclass
424class RoborockProductSpec(RoborockBase):
425    state: RoborockProductState
426    battery: dict | None = None
427    dry_countdown: dict | None = None
428    extra: dict | None = None
429    offpeak: dict | None = None
430    countdown: dict | None = None
431    mode: dict | None = None
432    ota_nfo: dict | None = None
433    pause: dict | None = None
434    program: dict | None = None
435    shutdown: dict | None = None
436    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
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class RoborockProduct(RoborockBase):
439@dataclass
440class RoborockProduct(RoborockBase):
441    id: int | None = None
442    name: str | None = None
443    model: str | None = None
444    packagename: str | None = None
445    ssid: str | None = None
446    picurl: str | None = None
447    cardpicurl: str | None = None
448    mediumCardpicurl: str | None = None
449    resetwifipicurl: str | None = None
450    configPicUrl: str | None = None
451    pluginPicUrl: str | None = None
452    resetwifitext: dict | None = None
453    tuyaid: str | None = None
454    status: int | None = None
455    rriotid: str | None = None
456    pictures: list | None = None
457    ncMode: str | None = None
458    scope: str | None = None
459    product_tags: list | None = None
460    agreements: list | None = None
461    cardspec: str | None = None
462    plugin_pic_url: str | None = None
463
464    @property
465    def product_nickname(self) -> RoborockProductNickname | None:
466        if self.cardspec:
467            return RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data"))
468        return None
469
470    def __repr__(self) -> str:
471        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
464    @property
465    def product_nickname(self) -> RoborockProductNickname | None:
466        if self.cardspec:
467            return RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data"))
468        return None
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class RoborockProductCategory(RoborockBase):
474@dataclass
475class RoborockProductCategory(RoborockBase):
476    id: int
477    display_name: str
478    icon_url: str
RoborockProductCategory(id: int, display_name: str, icon_url: str)
id: int
display_name: str
icon_url: str
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class RoborockCategoryDetail(RoborockBase):
481@dataclass
482class RoborockCategoryDetail(RoborockBase):
483    category: RoborockProductCategory
484    product_list: list[RoborockProduct]
RoborockCategoryDetail( category: RoborockProductCategory, product_list: list[RoborockProduct])
product_list: list[RoborockProduct]
Inherited Members
RoborockBase
from_dict
as_dict
@dataclass
class ProductResponse(RoborockBase):
487@dataclass
488class ProductResponse(RoborockBase):
489    category_detail_list: list[RoborockCategoryDetail]
ProductResponse( category_detail_list: list[RoborockCategoryDetail])
category_detail_list: list[RoborockCategoryDetail]
Inherited Members
RoborockBase
from_dict
as_dict