Skip to content

Commit f87f55c

Browse files
authored
fix: Update containers that __post_init__ to use properties (#503)
* fix: Update containers that __post_init__ to use properties This is done since with the new v1 api we reuse attributes and mutate them on update, so tis allows the properties to reamin fresh. * chore: fix typo * chore: Include atributes in repr computation * chore: update to get all properties at runtime * chore: fix lint errors
1 parent 56c648d commit f87f55c

File tree

2 files changed

+134
-73
lines changed

2 files changed

+134
-73
lines changed

roborock/containers.py

Lines changed: 132 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,32 @@ def _decamelize(s: str):
108108
return re.sub("([A-Z]+)", "_\\1", s).lower()
109109

110110

111-
@dataclass
111+
def _attr_repr(obj: Any) -> str:
112+
"""Return a string representation of the object including specified attributes.
113+
114+
This reproduces the default repr behavior of dataclasses, but also includes
115+
properties. This must be called by the child class's __repr__ method since
116+
the parent RoborockBase class does not know about the child class's attributes.
117+
"""
118+
# Reproduce default repr behavior
119+
parts = []
120+
for k in dir(obj):
121+
if k.startswith("_"):
122+
continue
123+
try:
124+
v = getattr(obj, k)
125+
except (RuntimeError, Exception):
126+
continue
127+
if callable(v):
128+
continue
129+
parts.append(f"{k}={v!r}")
130+
return f"{type(obj).__name__}({', '.join(parts)})"
131+
132+
133+
@dataclass(repr=False)
112134
class RoborockBase:
135+
"""Base class for all Roborock data classes."""
136+
113137
@staticmethod
114138
def _convert_to_class_obj(class_type: type, value):
115139
if get_origin(class_type) is list:
@@ -194,6 +218,9 @@ def end_time(self) -> datetime.time | None:
194218
else None
195219
)
196220

221+
def __repr__(self) -> str:
222+
return _attr_repr(self)
223+
197224

198225
@dataclass
199226
class Reference(RoborockBase):
@@ -346,7 +373,6 @@ class Status(RoborockBase):
346373
battery: int | None = None
347374
clean_time: int | None = None
348375
clean_area: int | None = None
349-
square_meter_clean_area: float | None = None
350376
error_code: RoborockErrorCode | None = None
351377
map_present: int | None = None
352378
in_cleaning: RoborockInCleaning | None = None
@@ -393,26 +419,36 @@ class Status(RoborockBase):
393419
dss: int | None = None
394420
common_status: int | None = None
395421
corner_clean_mode: int | None = None
396-
error_code_name: str | None = None
397-
state_name: str | None = None
398-
water_box_mode_name: str | None = None
399-
fan_power_options: list[str] = field(default_factory=list)
400-
fan_power_name: str | None = None
401-
mop_mode_name: str | None = None
402-
403-
def __post_init__(self) -> None:
404-
self.square_meter_clean_area = round(self.clean_area / 1000000, 1) if self.clean_area is not None else None
405-
if self.error_code is not None:
406-
self.error_code_name = self.error_code.name
407-
if self.state is not None:
408-
self.state_name = self.state.name
409-
if self.water_box_mode is not None:
410-
self.water_box_mode_name = self.water_box_mode.name
411-
if self.fan_power is not None:
412-
self.fan_power_options = self.fan_power.keys()
413-
self.fan_power_name = self.fan_power.name
414-
if self.mop_mode is not None:
415-
self.mop_mode_name = self.mop_mode.name
422+
423+
@property
424+
def square_meter_clean_area(self) -> float | None:
425+
return round(self.clean_area / 1000000, 1) if self.clean_area is not None else None
426+
427+
@property
428+
def error_code_name(self) -> str | None:
429+
return self.error_code.name if self.error_code else None
430+
431+
@property
432+
def state_name(self) -> str | None:
433+
return self.state.name if self.state else None
434+
435+
@property
436+
def water_box_mode_name(self) -> str | None:
437+
return self.water_box_mode.name if self.water_box_mode else None
438+
439+
@property
440+
def fan_power_options(self) -> list[str]:
441+
if self.fan_power is None:
442+
return []
443+
return list(self.fan_power.keys())
444+
445+
@property
446+
def fan_power_name(self) -> str | None:
447+
return self.fan_power.name if self.fan_power else None
448+
449+
@property
450+
def mop_mode_name(self) -> str | None:
451+
return self.mop_mode.name if self.mop_mode else None
416452

417453
def get_fan_speed_code(self, fan_speed: str) -> int:
418454
if self.fan_power is None:
@@ -438,6 +474,9 @@ def current_map(self) -> int | None:
438474
return map_flag
439475
return None
440476

477+
def __repr__(self) -> str:
478+
return _attr_repr(self)
479+
441480

442481
@dataclass
443482
class S4MaxStatus(Status):
@@ -588,28 +627,30 @@ class ValleyElectricityTimer(RoborockBaseTimer):
588627
class CleanSummary(RoborockBase):
589628
clean_time: int | None = None
590629
clean_area: int | None = None
591-
square_meter_clean_area: float | None = None
592630
clean_count: int | None = None
593631
dust_collection_count: int | None = None
594632
records: list[int] | None = None
595633
last_clean_t: int | None = None
596634

597-
def __post_init__(self) -> None:
635+
@property
636+
def square_meter_clean_area(self) -> float | None:
637+
"""Returns the clean area in square meters."""
598638
if isinstance(self.clean_area, list | str):
599639
_LOGGER.warning(f"Clean area is a unexpected type! Please give the following in a issue: {self.clean_area}")
600-
else:
601-
self.square_meter_clean_area = round(self.clean_area / 1000000, 1) if self.clean_area is not None else None
640+
return None
641+
return round(self.clean_area / 1000000, 1) if self.clean_area is not None else None
642+
643+
def __repr__(self) -> str:
644+
"""Return a string representation of the object including all attributes."""
645+
return _attr_repr(self)
602646

603647

604648
@dataclass
605649
class CleanRecord(RoborockBase):
606650
begin: int | None = None
607-
begin_datetime: datetime.datetime | None = None
608651
end: int | None = None
609-
end_datetime: datetime.datetime | None = None
610652
duration: int | None = None
611653
area: int | None = None
612-
square_meter_area: float | None = None
613654
error: int | None = None
614655
complete: int | None = None
615656
start_type: RoborockStartType | None = None
@@ -620,12 +661,20 @@ class CleanRecord(RoborockBase):
620661
wash_count: int | None = None
621662
map_flag: int | None = None
622663

623-
def __post_init__(self) -> None:
624-
self.square_meter_area = round(self.area / 1000000, 1) if self.area is not None else None
625-
self.begin_datetime = (
626-
datetime.datetime.fromtimestamp(self.begin).astimezone(timezone.utc) if self.begin else None
627-
)
628-
self.end_datetime = datetime.datetime.fromtimestamp(self.end).astimezone(timezone.utc) if self.end else None
664+
@property
665+
def square_meter_area(self) -> float | None:
666+
return round(self.area / 1000000, 1) if self.area is not None else None
667+
668+
@property
669+
def begin_datetime(self) -> datetime.datetime | None:
670+
return datetime.datetime.fromtimestamp(self.begin).astimezone(timezone.utc) if self.begin else None
671+
672+
@property
673+
def end_datetime(self) -> datetime.datetime | None:
674+
return datetime.datetime.fromtimestamp(self.end).astimezone(timezone.utc) if self.end else None
675+
676+
def __repr__(self) -> str:
677+
return _attr_repr(self)
629678

630679

631680
@dataclass
@@ -639,44 +688,49 @@ class Consumable(RoborockBase):
639688
dust_collection_work_times: int | None = None
640689
cleaning_brush_work_times: int | None = None
641690
moproller_work_time: int | None = None
642-
main_brush_time_left: int | None = None
643-
side_brush_time_left: int | None = None
644-
filter_time_left: int | None = None
645-
sensor_time_left: int | None = None
646-
strainer_time_left: int | None = None
647-
dust_collection_time_left: int | None = None
648-
cleaning_brush_time_left: int | None = None
649-
mop_roller_time_left: int | None = None
650-
651-
def __post_init__(self) -> None:
652-
self.main_brush_time_left = (
653-
MAIN_BRUSH_REPLACE_TIME - self.main_brush_work_time if self.main_brush_work_time is not None else None
654-
)
655-
self.side_brush_time_left = (
656-
SIDE_BRUSH_REPLACE_TIME - self.side_brush_work_time if self.side_brush_work_time is not None else None
657-
)
658-
self.filter_time_left = (
659-
FILTER_REPLACE_TIME - self.filter_work_time if self.filter_work_time is not None else None
660-
)
661-
self.sensor_time_left = (
662-
SENSOR_DIRTY_REPLACE_TIME - self.sensor_dirty_time if self.sensor_dirty_time is not None else None
663-
)
664-
self.strainer_time_left = (
665-
STRAINER_REPLACE_TIME - self.strainer_work_times if self.strainer_work_times is not None else None
666-
)
667-
self.dust_collection_time_left = (
691+
692+
@property
693+
def main_brush_time_left(self) -> int | None:
694+
return MAIN_BRUSH_REPLACE_TIME - self.main_brush_work_time if self.main_brush_work_time is not None else None
695+
696+
@property
697+
def side_brush_time_left(self) -> int | None:
698+
return SIDE_BRUSH_REPLACE_TIME - self.side_brush_work_time if self.side_brush_work_time is not None else None
699+
700+
@property
701+
def filter_time_left(self) -> int | None:
702+
return FILTER_REPLACE_TIME - self.filter_work_time if self.filter_work_time is not None else None
703+
704+
@property
705+
def sensor_time_left(self) -> int | None:
706+
return SENSOR_DIRTY_REPLACE_TIME - self.sensor_dirty_time if self.sensor_dirty_time is not None else None
707+
708+
@property
709+
def strainer_time_left(self) -> int | None:
710+
return STRAINER_REPLACE_TIME - self.strainer_work_times if self.strainer_work_times is not None else None
711+
712+
@property
713+
def dust_collection_time_left(self) -> int | None:
714+
return (
668715
DUST_COLLECTION_REPLACE_TIME - self.dust_collection_work_times
669716
if self.dust_collection_work_times is not None
670717
else None
671718
)
672-
self.cleaning_brush_time_left = (
719+
720+
@property
721+
def cleaning_brush_time_left(self) -> int | None:
722+
return (
673723
CLEANING_BRUSH_REPLACE_TIME - self.cleaning_brush_work_times
674724
if self.cleaning_brush_work_times is not None
675725
else None
676726
)
677-
self.mop_roller_time_left = (
678-
MOP_ROLLER_REPLACE_TIME - self.moproller_work_time if self.moproller_work_time is not None else None
679-
)
727+
728+
@property
729+
def mop_roller_time_left(self) -> int | None:
730+
return MOP_ROLLER_REPLACE_TIME - self.moproller_work_time if self.moproller_work_time is not None else None
731+
732+
def __repr__(self) -> str:
733+
return _attr_repr(self)
680734

681735

682736
@dataclass
@@ -760,11 +814,14 @@ class DeviceData(RoborockBase):
760814
device: HomeDataDevice
761815
model: str
762816
host: str | None = None
763-
product_nickname: RoborockProductNickname | None = None
764817
device_features: DeviceFeatures | None = None
765818

766-
def __post_init__(self):
767-
self.product_nickname = SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
819+
@property
820+
def product_nickname(self) -> RoborockProductNickname:
821+
return SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
822+
823+
def __repr__(self) -> str:
824+
return _attr_repr(self)
768825

769826

770827
@dataclass
@@ -849,11 +906,15 @@ class RoborockProduct(RoborockBase):
849906
agreements: list | None = None
850907
cardspec: str | None = None
851908
plugin_pic_url: str | None = None
852-
products_specification: RoborockProductSpec | None = None
853909

854-
def __post_init__(self):
910+
@property
911+
def product_nickname(self) -> RoborockProductNickname | None:
855912
if self.cardspec:
856-
self.products_specification = RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data"))
913+
return RoborockProductSpec.from_dict(json.loads(self.cardspec).get("data"))
914+
return None
915+
916+
def __repr__(self) -> str:
917+
return _attr_repr(self)
857918

858919

859920
@dataclass

tests/devices/__snapshots__/test_v1_device.ambr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# serializer version: 1
22
# name: test_device_trait_command_parsing[payload0-<lambda>]
3-
StatusTrait(msg_ver=2, msg_seq=515, state=<RoborockStateCode.charging: 8>, battery=100, clean_time=5405, clean_area=91287500, square_meter_clean_area=91.3, error_code=<RoborockErrorCode.none: 0>, map_present=1, in_cleaning=<RoborockInCleaning.complete: 0>, in_returning=0, in_fresh_state=1, lab_status=1, water_box_status=0, back_type=None, wash_phase=None, wash_ready=None, fan_power=<RoborockFanSpeedS7MaxV.custom: 106>, dnd_enabled=1, map_status=3, is_locating=0, lock_status=0, water_box_mode=<RoborockMopIntensityS7.custom: 204>, water_box_carriage_status=0, mop_forbidden_enable=0, camera_status=None, is_exploring=None, home_sec_status=None, home_sec_enable_password=None, adbumper_status=None, water_shortage_status=None, dock_type=None, dust_collection_status=None, auto_dust_collection=None, avoid_count=None, mop_mode=None, debug_mode=None, collision_avoid_status=None, switch_map_mode=None, dock_error_status=None, charge_status=None, unsave_map_reason=4, unsave_map_flag=0, wash_status=None, distance_off=0, in_warmup=None, dry_status=None, rdt=None, clean_percent=None, rss=None, dss=None, common_status=None, corner_clean_mode=None, error_code_name='none', state_name='charging', water_box_mode_name='custom', fan_power_options=['off', 'quiet', 'balanced', 'turbo', 'max', 'custom', 'max_plus'], fan_power_name='custom', mop_mode_name=None)
3+
StatusTrait(adbumper_status=None, auto_dust_collection=None, avoid_count=None, back_type=None, battery=100, camera_status=None, charge_status=None, clean_area=91287500, clean_percent=None, clean_time=5405, collision_avoid_status=None, command=<RoborockCommand.GET_STATUS: 'get_status'>, common_status=None, corner_clean_mode=None, current_map=0, debug_mode=None, distance_off=0, dnd_enabled=1, dock_error_status=None, dock_type=None, dry_status=None, dss=None, dust_collection_status=None, error_code=<RoborockErrorCode.none: 0>, error_code_name=None, fan_power=<RoborockFanSpeedS7MaxV.custom: 106>, fan_power_name='custom', fan_power_options=['off', 'quiet', 'balanced', 'turbo', 'max', 'custom', 'max_plus'], home_sec_enable_password=None, home_sec_status=None, in_cleaning=<RoborockInCleaning.complete: 0>, in_fresh_state=1, in_returning=0, in_warmup=None, is_exploring=None, is_locating=0, lab_status=1, lock_status=0, map_present=1, map_status=3, mop_forbidden_enable=0, mop_mode=None, mop_mode_name=None, msg_seq=515, msg_ver=2, rdt=None, rss=None, square_meter_clean_area=91.3, state=<RoborockStateCode.charging: 8>, state_name='charging', switch_map_mode=None, unsave_map_flag=0, unsave_map_reason=4, wash_phase=None, wash_ready=None, wash_status=None, water_box_carriage_status=0, water_box_mode=<RoborockMopIntensityS7.custom: 204>, water_box_mode_name='custom', water_box_status=0, water_shortage_status=None)
44
# ---
55
# name: test_device_trait_command_parsing[payload1-<lambda>]
66
DoNotDisturbTrait(start_hour=22, start_minute=0, end_hour=8, end_minute=0, enabled=1)
77
# ---
88
# name: test_device_trait_command_parsing[payload2-<lambda>]
9-
CleanSummaryTrait(clean_time=1442559, clean_area=24258125000, square_meter_clean_area=24258.1, clean_count=296, dust_collection_count=None, records=[1756848207, 1754930385, 1753203976, 1752183435, 1747427370, 1746204046, 1745601543, 1744387080, 1743528522, 1742489154, 1741022299, 1740433682, 1739902516, 1738875106, 1738864366, 1738620067, 1736873889, 1736197544, 1736121269, 1734458038], last_clean_t=None)
9+
CleanSummaryTrait(clean_area=24258125000, clean_count=296, clean_time=1442559, command=<RoborockCommand.GET_CLEAN_SUMMARY: 'get_clean_summary'>, dust_collection_count=None, last_clean_t=None, records=[1756848207, 1754930385, 1753203976, 1752183435, 1747427370, 1746204046, 1745601543, 1744387080, 1743528522, 1742489154, 1741022299, 1740433682, 1739902516, 1738875106, 1738864366, 1738620067, 1736873889, 1736197544, 1736121269, 1734458038], square_meter_clean_area=24258.1)
1010
# ---
1111
# name: test_device_trait_command_parsing[payload3-<lambda>]
1212
SoundVolumeTrait(volume=90)

0 commit comments

Comments
 (0)