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 Exception: # noqa: BLE001 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 63def field_metadata(**kwargs): 64 """Decorator to attach capability check metadata to a property. 65 66 This attaches a `_field_metadata` dictionary to the underlying getter function, 67 which is then preserved when decorated with `@property`. 68 69 Supported metadata keys: 70 - `feature` (str): Name of a capability property on `DeviceFeaturesTrait`. 71 - `dock_feature` (str): Name of a capability property on `RoborockDockFeatures`. 72 - `dps` (str/int): RoborockDataProtocol ID to check against supported schema IDs. 73 """ 74 75 def decorator(func): 76 func._field_metadata = kwargs 77 return func 78 79 return decorator 80 81 82@dataclass(repr=False) 83class RoborockBase: 84 """Base class for all Roborock data classes.""" 85 86 _missing_logged: ClassVar[set[str]] = set() 87 88 @staticmethod 89 def _convert_to_class_obj(class_type: type, value): 90 if get_origin(class_type) is list: 91 sub_type = get_args(class_type)[0] 92 return [RoborockBase._convert_to_class_obj(sub_type, obj) for obj in value] 93 if get_origin(class_type) is dict: 94 key_type, value_type = get_args(class_type) 95 if key_type is not None: 96 return {key_type(k): RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()} 97 return {k: RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()} 98 if inspect.isclass(class_type): 99 if issubclass(class_type, RoborockBase): 100 return class_type.from_dict(value) 101 if issubclass(class_type, RoborockModeEnum): 102 return class_type.from_code(value) 103 if class_type is Any or type(class_type) is str: 104 return value 105 return class_type(value) # type: ignore[call-arg] 106 107 @classmethod 108 def from_dict(cls, data: dict[str, Any]): 109 """Create an instance of the class from a dictionary.""" 110 if not isinstance(data, dict): 111 return None 112 field_types = {field.name: field.type for field in dataclasses.fields(cls)} 113 normalized_data: dict[str, Any] = {} 114 for orig_key, value in data.items(): 115 key = _decamelize(orig_key) 116 if field_types.get(key) is None: 117 if (log_key := f"{cls.__name__}.{key}") not in RoborockBase._missing_logged: 118 _LOGGER.debug( 119 "Key '%s' (decamelized: '%s') not found in %s fields, skipping", 120 orig_key, 121 key, 122 cls.__name__, 123 ) 124 RoborockBase._missing_logged.add(log_key) 125 continue 126 normalized_data[key] = value 127 128 result = RoborockBase.convert_dict(field_types, normalized_data) 129 return cls(**result) 130 131 @staticmethod 132 def convert_dict(types_map: dict[Any, type], data: dict[Any, Any]) -> dict[Any, Any]: 133 """Generic helper to convert a dictionary of values based on a schema map of types. 134 135 This is meant to be used by traits that use dataclass reflection similar to 136 `Roborock.from_dict` to merge in new data updates. 137 """ 138 result: dict[Any, Any] = {} 139 for key, value in data.items(): 140 if key not in types_map: 141 continue 142 field_type = types_map[key] 143 if value == "None" or value is None: 144 result[key] = None 145 continue 146 if isinstance(field_type, types.UnionType): 147 for subtype in get_args(field_type): 148 if subtype is types.NoneType: 149 continue 150 try: 151 result[key] = RoborockBase._convert_to_class_obj(subtype, value) 152 break 153 except Exception: 154 _LOGGER.exception(f"Failed to convert {key} with value {value} to type {subtype}") 155 continue 156 else: 157 try: 158 result[key] = RoborockBase._convert_to_class_obj(field_type, value) 159 except Exception: 160 _LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}") 161 continue 162 163 return result 164 165 def as_dict(self, exclude: set[str] | None = None) -> dict: 166 exclude_set = exclude or set() 167 return asdict( 168 self, 169 dict_factory=lambda _fields: { 170 _camelize(key): value.value if isinstance(value, Enum) else value 171 for (key, value) in _fields 172 if value is not None and key not in exclude_set 173 }, 174 ) 175 176 177@dataclass 178class RoborockBaseTimer(RoborockBase): 179 start_hour: int | None = None 180 start_minute: int | None = None 181 end_hour: int | None = None 182 end_minute: int | None = None 183 enabled: int | None = None 184 185 @property 186 def start_time(self) -> datetime.time | None: 187 return ( 188 datetime.time(hour=self.start_hour, minute=self.start_minute) 189 if self.start_hour is not None and self.start_minute is not None 190 else None 191 ) 192 193 @property 194 def end_time(self) -> datetime.time | None: 195 return ( 196 datetime.time(hour=self.end_hour, minute=self.end_minute) 197 if self.end_hour is not None and self.end_minute is not None 198 else None 199 ) 200 201 def as_list(self) -> list: 202 return [self.start_hour, self.start_minute, self.end_hour, self.end_minute] 203 204 def __repr__(self) -> str: 205 return _attr_repr(self) 206 207 208@dataclass 209class Reference(RoborockBase): 210 r: str | None = None 211 a: str | None = None 212 m: str | None = None 213 l: str | None = None # noqa: E741 214 215 216@dataclass 217class RRiot(RoborockBase): 218 u: str 219 s: str 220 h: str 221 k: str 222 r: Reference 223 224 225@dataclass 226class UserData(RoborockBase): 227 rriot: RRiot 228 uid: int | None = None 229 tokentype: str | None = None 230 token: str | None = None 231 rruid: str | None = None 232 region: str | None = None 233 countrycode: str | None = None 234 country: str | None = None 235 nickname: str | None = None 236 tuya_device_state: int | None = None 237 avatarurl: str | None = None 238 239 240@dataclass 241class HomeDataProductSchema(RoborockBase): 242 id: Any | None = None 243 name: Any | None = None 244 code: Any | None = None 245 mode: Any | None = None 246 type: Any | None = None 247 product_property: Any | None = None 248 property: Any | None = None 249 desc: Any | None = None 250 251 252@dataclass 253class HomeDataProduct(RoborockBase): 254 id: str 255 name: str 256 model: str 257 category: RoborockCategory 258 code: str | None = None 259 icon_url: str | None = None 260 attribute: Any | None = None 261 capability: int | None = None 262 schema: list[HomeDataProductSchema] | None = None 263 264 @property 265 def product_nickname(self) -> RoborockProductNickname: 266 return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS) 267 268 def summary_info(self) -> str: 269 """Return a string with key product information for logging purposes.""" 270 return f"{self.name} (model={self.model}, category={self.category})" 271 272 @cached_property 273 def supported_schema_codes(self) -> set[str]: 274 """Return a set of schema codes that are supported by the device. 275 276 These correspond with string field names like "state" or "error_code" that 277 correspond to RoborockDataProtocol or RoborockB01Protocol code values. 278 """ 279 if self.schema is None: 280 return set() 281 return {schema.code for schema in self.schema if schema.code is not None} 282 283 @cached_property 284 def supported_schema_ids(self) -> set[int]: 285 """Return a set of schema IDs (DPS integers) that are supported by the device. 286 287 These correspond to RoborockMessageProtocol and RoborockDataProtocol or 288 RoborockB01Protocol enum number values (depends on the device protocol versions). 289 """ 290 if self.schema is None: 291 return set() 292 return {int(schema.id) for schema in self.schema if schema.id is not None} 293 294 295@dataclass 296class HomeDataDevice(RoborockBase): 297 duid: str 298 name: str 299 local_key: str 300 product_id: str 301 fv: str | None = None 302 attribute: Any | None = None 303 active_time: int | None = None 304 runtime_env: Any | None = None 305 time_zone_id: str | None = None 306 icon_url: str | None = None 307 lon: Any | None = None 308 lat: Any | None = None 309 share: Any | None = None 310 share_time: Any | None = None 311 online: bool | None = None 312 pv: str | None = None 313 room_id: Any | None = None 314 tuya_uuid: Any | None = None 315 tuya_migrated: bool | None = None 316 extra: Any | None = None 317 sn: str | None = None 318 feature_set: str | None = None 319 new_feature_set: str | None = None 320 device_status: dict | None = None 321 silent_ota_switch: bool | None = None 322 setting: Any | None = None 323 f: bool | None = None 324 create_time: int | None = None 325 cid: str | None = None 326 share_type: Any | None = None 327 share_expired_time: int | None = None 328 329 def summary_info(self) -> str: 330 """Return a string with key device information for logging purposes.""" 331 return f"{self.name} (pv={self.pv}, fv={self.fv}, online={self.online})" 332 333 334@dataclass 335class HomeDataRoom(RoborockBase): 336 id: int 337 name: str 338 339 @property 340 def iot_id(self) -> str: 341 """Return the room's ID as a string IOT ID.""" 342 return str(self.id) 343 344 345@dataclass 346class HomeDataScene(RoborockBase): 347 id: int 348 name: str 349 350 351@dataclass 352class FirmwareInfo(RoborockBase): 353 """Firmware/OTA info from the cloud (`ota/firmware/{duid}/updatev2`).""" 354 355 version: str | None = None 356 """Latest available firmware version.""" 357 current_version: str | None = None 358 """Currently installed firmware version.""" 359 updatable: bool | None = None 360 """Whether a newer firmware is available to install.""" 361 desc: str | None = None 362 """Release notes / description.""" 363 release_time: str | None = None 364 """Release date of the available firmware as an ISO-8601 date (``YYYY-MM-DD``).""" 365 force_update: bool | None = None 366 """Whether the update is mandatory (cannot be skipped).""" 367 368 369@dataclass 370class HomeDataSchedule(RoborockBase): 371 id: int 372 cron: str 373 repeated: bool 374 enabled: bool 375 param: dict | None = None 376 377 378@dataclass 379class HomeData(RoborockBase): 380 id: int 381 name: str 382 products: list[HomeDataProduct] = field(default_factory=list) 383 devices: list[HomeDataDevice] = field(default_factory=list) 384 received_devices: list[HomeDataDevice] = field(default_factory=list) 385 lon: Any | None = None 386 lat: Any | None = None 387 geo_name: Any | None = None 388 rooms: list[HomeDataRoom] = field(default_factory=list) 389 390 def get_all_devices(self) -> list[HomeDataDevice]: 391 devices = [] 392 if self.devices is not None: 393 devices += self.devices 394 if self.received_devices is not None: 395 devices += self.received_devices 396 return devices 397 398 @cached_property 399 def product_map(self) -> dict[str, HomeDataProduct]: 400 """Returns a dictionary of product IDs to HomeDataProduct objects.""" 401 return {product.id: product for product in self.products} 402 403 @cached_property 404 def device_products(self) -> dict[str, tuple[HomeDataDevice, HomeDataProduct]]: 405 """Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects.""" 406 product_map = self.product_map 407 return { 408 device.duid: (device, product) 409 for device in self.get_all_devices() 410 if (product := product_map.get(device.product_id)) is not None 411 } 412 413 @property 414 def rooms_map(self) -> dict[str, HomeDataRoom]: 415 """Returns a dictionary of Room iot_id to rooms""" 416 return {room.iot_id: room for room in self.rooms} 417 418 @property 419 def rooms_name_map(self) -> dict[str, str]: 420 """Returns a dictionary of Room iot_id to room names.""" 421 return {room.iot_id: room.name for room in self.rooms} 422 423 424@dataclass 425class LoginData(RoborockBase): 426 user_data: UserData 427 email: str 428 home_data: HomeData | None = None 429 430 431@dataclass 432class DeviceData(RoborockBase): 433 device: HomeDataDevice 434 model: str 435 host: str | None = None 436 437 @property 438 def product_nickname(self) -> RoborockProductNickname: 439 return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS) 440 441 def __repr__(self) -> str: 442 return _attr_repr(self) 443 444 445@dataclass 446class RoomMapping(RoborockBase): 447 segment_id: int 448 iot_id: str 449 450 451@dataclass 452class NamedRoomMapping(RoomMapping): 453 """Dataclass representing a mapping of a room segment to a name. 454 455 The name information is not provided by the device directly, but is provided 456 from the HomeData based on the iot_id from the room. 457 """ 458 459 @property 460 def name(self) -> str: 461 """The human-readable name of the room, or a default name if not available.""" 462 return self.raw_name or f"Room {self.segment_id}" 463 464 raw_name: str | None = None 465 """The raw name of the room, as provided by the device.""" 466 467 468@dataclass 469class CombinedMapInfo(RoborockBase): 470 """Data structure for caching home information. 471 472 This is not provided directly by the API, but is a combination of map data 473 and room data to provide a more useful structure. 474 """ 475 476 map_flag: int 477 """The map identifier.""" 478 479 name: str 480 """The name of the map from MultiMapsListMapInfo.""" 481 482 rooms: list[NamedRoomMapping] 483 """The list of rooms in the map.""" 484 485 @property 486 def rooms_map(self) -> dict[int, NamedRoomMapping]: 487 """Returns a mapping of segment_id to NamedRoomMapping.""" 488 return {room.segment_id: room for room in self.rooms} 489 490 491@dataclass 492class BroadcastMessage(RoborockBase): 493 duid: str 494 ip: str 495 version: bytes 496 497 498class ServerTimer(NamedTuple): 499 id: str 500 status: str 501 dontknow: int 502 503 504@dataclass 505class RoborockProductStateValue(RoborockBase): 506 value: list 507 desc: dict 508 509 510@dataclass 511class RoborockProductState(RoborockBase): 512 dps: int 513 desc: dict 514 value: list[RoborockProductStateValue] 515 516 517@dataclass 518class RoborockProductSpec(RoborockBase): 519 state: RoborockProductState 520 battery: dict | None = None 521 dry_countdown: dict | None = None 522 extra: dict | None = None 523 offpeak: dict | None = None 524 countdown: dict | None = None 525 mode: dict | None = None 526 ota_nfo: dict | None = None 527 pause: dict | None = None 528 program: dict | None = None 529 shutdown: dict | None = None 530 washing_left: dict | None = None 531 532 533@dataclass 534class RoborockProduct(RoborockBase): 535 id: int | None = None 536 name: str | None = None 537 model: str | None = None 538 packagename: str | None = None 539 ssid: str | None = None 540 picurl: str | None = None 541 cardpicurl: str | None = None 542 mediumCardpicurl: str | None = None 543 resetwifipicurl: str | None = None 544 configPicUrl: str | None = None 545 pluginPicUrl: str | None = None 546 resetwifitext: dict | None = None 547 tuyaid: str | None = None 548 status: int | None = None 549 rriotid: str | None = None 550 pictures: list | None = None 551 ncMode: str | None = None 552 scope: str | None = None 553 product_tags: list | None = None 554 agreements: list | None = None 555 cardspec: str | None = None 556 plugin_pic_url: str | None = None 557 558 @property 559 def product_nickname(self) -> RoborockProductNickname | None: 560 if self.cardspec: 561 return RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data")) 562 return None 563 564 def __repr__(self) -> str: 565 return _attr_repr(self) 566 567 568@dataclass 569class RoborockProductCategory(RoborockBase): 570 id: int 571 display_name: str 572 icon_url: str 573 574 575@dataclass 576class RoborockCategoryDetail(RoborockBase): 577 category: RoborockProductCategory 578 product_list: list[RoborockProduct] 579 580 581@dataclass 582class ProductResponse(RoborockBase): 583 category_detail_list: list[RoborockCategoryDetail]
64def field_metadata(**kwargs): 65 """Decorator to attach capability check metadata to a property. 66 67 This attaches a `_field_metadata` dictionary to the underlying getter function, 68 which is then preserved when decorated with `@property`. 69 70 Supported metadata keys: 71 - `feature` (str): Name of a capability property on `DeviceFeaturesTrait`. 72 - `dock_feature` (str): Name of a capability property on `RoborockDockFeatures`. 73 - `dps` (str/int): RoborockDataProtocol ID to check against supported schema IDs. 74 """ 75 76 def decorator(func): 77 func._field_metadata = kwargs 78 return func 79 80 return decorator
Decorator to attach capability check metadata to a property.
This attaches a _field_metadata dictionary to the underlying getter function,
which is then preserved when decorated with @property.
Supported metadata keys:
feature(str): Name of a capability property onDeviceFeaturesTrait.dock_feature(str): Name of a capability property onRoborockDockFeatures.dps(str/int): RoborockDataProtocol ID to check against supported schema IDs.
83@dataclass(repr=False) 84class RoborockBase: 85 """Base class for all Roborock data classes.""" 86 87 _missing_logged: ClassVar[set[str]] = set() 88 89 @staticmethod 90 def _convert_to_class_obj(class_type: type, value): 91 if get_origin(class_type) is list: 92 sub_type = get_args(class_type)[0] 93 return [RoborockBase._convert_to_class_obj(sub_type, obj) for obj in value] 94 if get_origin(class_type) is dict: 95 key_type, value_type = get_args(class_type) 96 if key_type is not None: 97 return {key_type(k): RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()} 98 return {k: RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()} 99 if inspect.isclass(class_type): 100 if issubclass(class_type, RoborockBase): 101 return class_type.from_dict(value) 102 if issubclass(class_type, RoborockModeEnum): 103 return class_type.from_code(value) 104 if class_type is Any or type(class_type) is str: 105 return value 106 return class_type(value) # type: ignore[call-arg] 107 108 @classmethod 109 def from_dict(cls, data: dict[str, Any]): 110 """Create an instance of the class from a dictionary.""" 111 if not isinstance(data, dict): 112 return None 113 field_types = {field.name: field.type for field in dataclasses.fields(cls)} 114 normalized_data: dict[str, Any] = {} 115 for orig_key, value in data.items(): 116 key = _decamelize(orig_key) 117 if field_types.get(key) is None: 118 if (log_key := f"{cls.__name__}.{key}") not in RoborockBase._missing_logged: 119 _LOGGER.debug( 120 "Key '%s' (decamelized: '%s') not found in %s fields, skipping", 121 orig_key, 122 key, 123 cls.__name__, 124 ) 125 RoborockBase._missing_logged.add(log_key) 126 continue 127 normalized_data[key] = value 128 129 result = RoborockBase.convert_dict(field_types, normalized_data) 130 return cls(**result) 131 132 @staticmethod 133 def convert_dict(types_map: dict[Any, type], data: dict[Any, Any]) -> dict[Any, Any]: 134 """Generic helper to convert a dictionary of values based on a schema map of types. 135 136 This is meant to be used by traits that use dataclass reflection similar to 137 `Roborock.from_dict` to merge in new data updates. 138 """ 139 result: dict[Any, Any] = {} 140 for key, value in data.items(): 141 if key not in types_map: 142 continue 143 field_type = types_map[key] 144 if value == "None" or value is None: 145 result[key] = None 146 continue 147 if isinstance(field_type, types.UnionType): 148 for subtype in get_args(field_type): 149 if subtype is types.NoneType: 150 continue 151 try: 152 result[key] = RoborockBase._convert_to_class_obj(subtype, value) 153 break 154 except Exception: 155 _LOGGER.exception(f"Failed to convert {key} with value {value} to type {subtype}") 156 continue 157 else: 158 try: 159 result[key] = RoborockBase._convert_to_class_obj(field_type, value) 160 except Exception: 161 _LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}") 162 continue 163 164 return result 165 166 def as_dict(self, exclude: set[str] | None = None) -> dict: 167 exclude_set = exclude or set() 168 return asdict( 169 self, 170 dict_factory=lambda _fields: { 171 _camelize(key): value.value if isinstance(value, Enum) else value 172 for (key, value) in _fields 173 if value is not None and key not in exclude_set 174 }, 175 )
Base class for all Roborock data classes.
108 @classmethod 109 def from_dict(cls, data: dict[str, Any]): 110 """Create an instance of the class from a dictionary.""" 111 if not isinstance(data, dict): 112 return None 113 field_types = {field.name: field.type for field in dataclasses.fields(cls)} 114 normalized_data: dict[str, Any] = {} 115 for orig_key, value in data.items(): 116 key = _decamelize(orig_key) 117 if field_types.get(key) is None: 118 if (log_key := f"{cls.__name__}.{key}") not in RoborockBase._missing_logged: 119 _LOGGER.debug( 120 "Key '%s' (decamelized: '%s') not found in %s fields, skipping", 121 orig_key, 122 key, 123 cls.__name__, 124 ) 125 RoborockBase._missing_logged.add(log_key) 126 continue 127 normalized_data[key] = value 128 129 result = RoborockBase.convert_dict(field_types, normalized_data) 130 return cls(**result)
Create an instance of the class from a dictionary.
132 @staticmethod 133 def convert_dict(types_map: dict[Any, type], data: dict[Any, Any]) -> dict[Any, Any]: 134 """Generic helper to convert a dictionary of values based on a schema map of types. 135 136 This is meant to be used by traits that use dataclass reflection similar to 137 `Roborock.from_dict` to merge in new data updates. 138 """ 139 result: dict[Any, Any] = {} 140 for key, value in data.items(): 141 if key not in types_map: 142 continue 143 field_type = types_map[key] 144 if value == "None" or value is None: 145 result[key] = None 146 continue 147 if isinstance(field_type, types.UnionType): 148 for subtype in get_args(field_type): 149 if subtype is types.NoneType: 150 continue 151 try: 152 result[key] = RoborockBase._convert_to_class_obj(subtype, value) 153 break 154 except Exception: 155 _LOGGER.exception(f"Failed to convert {key} with value {value} to type {subtype}") 156 continue 157 else: 158 try: 159 result[key] = RoborockBase._convert_to_class_obj(field_type, value) 160 except Exception: 161 _LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}") 162 continue 163 164 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.
166 def as_dict(self, exclude: set[str] | None = None) -> dict: 167 exclude_set = exclude or set() 168 return asdict( 169 self, 170 dict_factory=lambda _fields: { 171 _camelize(key): value.value if isinstance(value, Enum) else value 172 for (key, value) in _fields 173 if value is not None and key not in exclude_set 174 }, 175 )
178@dataclass 179class RoborockBaseTimer(RoborockBase): 180 start_hour: int | None = None 181 start_minute: int | None = None 182 end_hour: int | None = None 183 end_minute: int | None = None 184 enabled: int | None = None 185 186 @property 187 def start_time(self) -> datetime.time | None: 188 return ( 189 datetime.time(hour=self.start_hour, minute=self.start_minute) 190 if self.start_hour is not None and self.start_minute is not None 191 else None 192 ) 193 194 @property 195 def end_time(self) -> datetime.time | None: 196 return ( 197 datetime.time(hour=self.end_hour, minute=self.end_minute) 198 if self.end_hour is not None and self.end_minute is not None 199 else None 200 ) 201 202 def as_list(self) -> list: 203 return [self.start_hour, self.start_minute, self.end_hour, self.end_minute] 204 205 def __repr__(self) -> str: 206 return _attr_repr(self)
Inherited Members
209@dataclass 210class Reference(RoborockBase): 211 r: str | None = None 212 a: str | None = None 213 m: str | None = None 214 l: str | None = None # noqa: E741
Inherited Members
217@dataclass 218class RRiot(RoborockBase): 219 u: str 220 s: str 221 h: str 222 k: str 223 r: Reference
Inherited Members
226@dataclass 227class UserData(RoborockBase): 228 rriot: RRiot 229 uid: int | None = None 230 tokentype: str | None = None 231 token: str | None = None 232 rruid: str | None = None 233 region: str | None = None 234 countrycode: str | None = None 235 country: str | None = None 236 nickname: str | None = None 237 tuya_device_state: int | None = None 238 avatarurl: str | None = None
Inherited Members
241@dataclass 242class HomeDataProductSchema(RoborockBase): 243 id: Any | None = None 244 name: Any | None = None 245 code: Any | None = None 246 mode: Any | None = None 247 type: Any | None = None 248 product_property: Any | None = None 249 property: Any | None = None 250 desc: Any | None = None
Inherited Members
253@dataclass 254class HomeDataProduct(RoborockBase): 255 id: str 256 name: str 257 model: str 258 category: RoborockCategory 259 code: str | None = None 260 icon_url: str | None = None 261 attribute: Any | None = None 262 capability: int | None = None 263 schema: list[HomeDataProductSchema] | None = None 264 265 @property 266 def product_nickname(self) -> RoborockProductNickname: 267 return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS) 268 269 def summary_info(self) -> str: 270 """Return a string with key product information for logging purposes.""" 271 return f"{self.name} (model={self.model}, category={self.category})" 272 273 @cached_property 274 def supported_schema_codes(self) -> set[str]: 275 """Return a set of schema codes that are supported by the device. 276 277 These correspond with string field names like "state" or "error_code" that 278 correspond to RoborockDataProtocol or RoborockB01Protocol code values. 279 """ 280 if self.schema is None: 281 return set() 282 return {schema.code for schema in self.schema if schema.code is not None} 283 284 @cached_property 285 def supported_schema_ids(self) -> set[int]: 286 """Return a set of schema IDs (DPS integers) that are supported by the device. 287 288 These correspond to RoborockMessageProtocol and RoborockDataProtocol or 289 RoborockB01Protocol enum number values (depends on the device protocol versions). 290 """ 291 if self.schema is None: 292 return set() 293 return {int(schema.id) for schema in self.schema if schema.id is not None}
269 def summary_info(self) -> str: 270 """Return a string with key product information for logging purposes.""" 271 return f"{self.name} (model={self.model}, category={self.category})"
Return a string with key product information for logging purposes.
273 @cached_property 274 def supported_schema_codes(self) -> set[str]: 275 """Return a set of schema codes that are supported by the device. 276 277 These correspond with string field names like "state" or "error_code" that 278 correspond to RoborockDataProtocol or RoborockB01Protocol code values. 279 """ 280 if self.schema is None: 281 return set() 282 return {schema.code for schema in self.schema if schema.code is not None}
Return a set of schema codes that are supported by the device.
These correspond with string field names like "state" or "error_code" that correspond to RoborockDataProtocol or RoborockB01Protocol code values.
284 @cached_property 285 def supported_schema_ids(self) -> set[int]: 286 """Return a set of schema IDs (DPS integers) that are supported by the device. 287 288 These correspond to RoborockMessageProtocol and RoborockDataProtocol or 289 RoborockB01Protocol enum number values (depends on the device protocol versions). 290 """ 291 if self.schema is None: 292 return set() 293 return {int(schema.id) for schema in self.schema if schema.id is not None}
Return a set of schema IDs (DPS integers) that are supported by the device.
These correspond to RoborockMessageProtocol and RoborockDataProtocol or RoborockB01Protocol enum number values (depends on the device protocol versions).
Inherited Members
296@dataclass 297class HomeDataDevice(RoborockBase): 298 duid: str 299 name: str 300 local_key: str 301 product_id: str 302 fv: str | None = None 303 attribute: Any | None = None 304 active_time: int | None = None 305 runtime_env: Any | None = None 306 time_zone_id: str | None = None 307 icon_url: str | None = None 308 lon: Any | None = None 309 lat: Any | None = None 310 share: Any | None = None 311 share_time: Any | None = None 312 online: bool | None = None 313 pv: str | None = None 314 room_id: Any | None = None 315 tuya_uuid: Any | None = None 316 tuya_migrated: bool | None = None 317 extra: Any | None = None 318 sn: str | None = None 319 feature_set: str | None = None 320 new_feature_set: str | None = None 321 device_status: dict | None = None 322 silent_ota_switch: bool | None = None 323 setting: Any | None = None 324 f: bool | None = None 325 create_time: int | None = None 326 cid: str | None = None 327 share_type: Any | None = None 328 share_expired_time: int | None = None 329 330 def summary_info(self) -> str: 331 """Return a string with key device information for logging purposes.""" 332 return f"{self.name} (pv={self.pv}, fv={self.fv}, online={self.online})"
330 def summary_info(self) -> str: 331 """Return a string with key device information for logging purposes.""" 332 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
335@dataclass 336class HomeDataRoom(RoborockBase): 337 id: int 338 name: str 339 340 @property 341 def iot_id(self) -> str: 342 """Return the room's ID as a string IOT ID.""" 343 return str(self.id)
340 @property 341 def iot_id(self) -> str: 342 """Return the room's ID as a string IOT ID.""" 343 return str(self.id)
Return the room's ID as a string IOT ID.
Inherited Members
Inherited Members
352@dataclass 353class FirmwareInfo(RoborockBase): 354 """Firmware/OTA info from the cloud (`ota/firmware/{duid}/updatev2`).""" 355 356 version: str | None = None 357 """Latest available firmware version.""" 358 current_version: str | None = None 359 """Currently installed firmware version.""" 360 updatable: bool | None = None 361 """Whether a newer firmware is available to install.""" 362 desc: str | None = None 363 """Release notes / description.""" 364 release_time: str | None = None 365 """Release date of the available firmware as an ISO-8601 date (``YYYY-MM-DD``).""" 366 force_update: bool | None = None 367 """Whether the update is mandatory (cannot be skipped)."""
Firmware/OTA info from the cloud (ota/firmware/{duid}/updatev2).
Release date of the available firmware as an ISO-8601 date (YYYY-MM-DD).
Inherited Members
370@dataclass 371class HomeDataSchedule(RoborockBase): 372 id: int 373 cron: str 374 repeated: bool 375 enabled: bool 376 param: dict | None = None
Inherited Members
379@dataclass 380class HomeData(RoborockBase): 381 id: int 382 name: str 383 products: list[HomeDataProduct] = field(default_factory=list) 384 devices: list[HomeDataDevice] = field(default_factory=list) 385 received_devices: list[HomeDataDevice] = field(default_factory=list) 386 lon: Any | None = None 387 lat: Any | None = None 388 geo_name: Any | None = None 389 rooms: list[HomeDataRoom] = field(default_factory=list) 390 391 def get_all_devices(self) -> list[HomeDataDevice]: 392 devices = [] 393 if self.devices is not None: 394 devices += self.devices 395 if self.received_devices is not None: 396 devices += self.received_devices 397 return devices 398 399 @cached_property 400 def product_map(self) -> dict[str, HomeDataProduct]: 401 """Returns a dictionary of product IDs to HomeDataProduct objects.""" 402 return {product.id: product for product in self.products} 403 404 @cached_property 405 def device_products(self) -> dict[str, tuple[HomeDataDevice, HomeDataProduct]]: 406 """Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects.""" 407 product_map = self.product_map 408 return { 409 device.duid: (device, product) 410 for device in self.get_all_devices() 411 if (product := product_map.get(device.product_id)) is not None 412 } 413 414 @property 415 def rooms_map(self) -> dict[str, HomeDataRoom]: 416 """Returns a dictionary of Room iot_id to rooms""" 417 return {room.iot_id: room for room in self.rooms} 418 419 @property 420 def rooms_name_map(self) -> dict[str, str]: 421 """Returns a dictionary of Room iot_id to room names.""" 422 return {room.iot_id: room.name for room in self.rooms}
399 @cached_property 400 def product_map(self) -> dict[str, HomeDataProduct]: 401 """Returns a dictionary of product IDs to HomeDataProduct objects.""" 402 return {product.id: product for product in self.products}
Returns a dictionary of product IDs to HomeDataProduct objects.
404 @cached_property 405 def device_products(self) -> dict[str, tuple[HomeDataDevice, HomeDataProduct]]: 406 """Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects.""" 407 product_map = self.product_map 408 return { 409 device.duid: (device, product) 410 for device in self.get_all_devices() 411 if (product := product_map.get(device.product_id)) is not None 412 }
Returns a dictionary of device DUIDs to HomeDataDeviceProduct objects.
414 @property 415 def rooms_map(self) -> dict[str, HomeDataRoom]: 416 """Returns a dictionary of Room iot_id to rooms""" 417 return {room.iot_id: room for room in self.rooms}
Returns a dictionary of Room iot_id to rooms
419 @property 420 def rooms_name_map(self) -> dict[str, str]: 421 """Returns a dictionary of Room iot_id to room names.""" 422 return {room.iot_id: room.name for room in self.rooms}
Returns a dictionary of Room iot_id to room names.
Inherited Members
425@dataclass 426class LoginData(RoborockBase): 427 user_data: UserData 428 email: str 429 home_data: HomeData | None = None
Inherited Members
432@dataclass 433class DeviceData(RoborockBase): 434 device: HomeDataDevice 435 model: str 436 host: str | None = None 437 438 @property 439 def product_nickname(self) -> RoborockProductNickname: 440 return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS) 441 442 def __repr__(self) -> str: 443 return _attr_repr(self)
Inherited Members
Inherited Members
452@dataclass 453class NamedRoomMapping(RoomMapping): 454 """Dataclass representing a mapping of a room segment to a name. 455 456 The name information is not provided by the device directly, but is provided 457 from the HomeData based on the iot_id from the room. 458 """ 459 460 @property 461 def name(self) -> str: 462 """The human-readable name of the room, or a default name if not available.""" 463 return self.raw_name or f"Room {self.segment_id}" 464 465 raw_name: str | None = None 466 """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.
460 @property 461 def name(self) -> str: 462 """The human-readable name of the room, or a default name if not available.""" 463 return self.raw_name or f"Room {self.segment_id}"
The human-readable name of the room, or a default name if not available.
Inherited Members
469@dataclass 470class CombinedMapInfo(RoborockBase): 471 """Data structure for caching home information. 472 473 This is not provided directly by the API, but is a combination of map data 474 and room data to provide a more useful structure. 475 """ 476 477 map_flag: int 478 """The map identifier.""" 479 480 name: str 481 """The name of the map from MultiMapsListMapInfo.""" 482 483 rooms: list[NamedRoomMapping] 484 """The list of rooms in the map.""" 485 486 @property 487 def rooms_map(self) -> dict[int, NamedRoomMapping]: 488 """Returns a mapping of segment_id to NamedRoomMapping.""" 489 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.
486 @property 487 def rooms_map(self) -> dict[int, NamedRoomMapping]: 488 """Returns a mapping of segment_id to NamedRoomMapping.""" 489 return {room.segment_id: room for room in self.rooms}
Returns a mapping of segment_id to NamedRoomMapping.
Inherited Members
Inherited Members
ServerTimer(id, status, dontknow)
Inherited Members
511@dataclass 512class RoborockProductState(RoborockBase): 513 dps: int 514 desc: dict 515 value: list[RoborockProductStateValue]
Inherited Members
518@dataclass 519class RoborockProductSpec(RoborockBase): 520 state: RoborockProductState 521 battery: dict | None = None 522 dry_countdown: dict | None = None 523 extra: dict | None = None 524 offpeak: dict | None = None 525 countdown: dict | None = None 526 mode: dict | None = None 527 ota_nfo: dict | None = None 528 pause: dict | None = None 529 program: dict | None = None 530 shutdown: dict | None = None 531 washing_left: dict | None = None
Inherited Members
534@dataclass 535class RoborockProduct(RoborockBase): 536 id: int | None = None 537 name: str | None = None 538 model: str | None = None 539 packagename: str | None = None 540 ssid: str | None = None 541 picurl: str | None = None 542 cardpicurl: str | None = None 543 mediumCardpicurl: str | None = None 544 resetwifipicurl: str | None = None 545 configPicUrl: str | None = None 546 pluginPicUrl: str | None = None 547 resetwifitext: dict | None = None 548 tuyaid: str | None = None 549 status: int | None = None 550 rriotid: str | None = None 551 pictures: list | None = None 552 ncMode: str | None = None 553 scope: str | None = None 554 product_tags: list | None = None 555 agreements: list | None = None 556 cardspec: str | None = None 557 plugin_pic_url: str | None = None 558 559 @property 560 def product_nickname(self) -> RoborockProductNickname | None: 561 if self.cardspec: 562 return RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data")) 563 return None 564 565 def __repr__(self) -> str: 566 return _attr_repr(self)
Inherited Members
569@dataclass 570class RoborockProductCategory(RoborockBase): 571 id: int 572 display_name: str 573 icon_url: str
Inherited Members
576@dataclass 577class RoborockCategoryDetail(RoborockBase): 578 category: RoborockProductCategory 579 product_list: list[RoborockProduct]
Inherited Members
582@dataclass 583class ProductResponse(RoborockBase): 584 category_detail_list: list[RoborockCategoryDetail]