diff --git a/roborock/data/code_mappings.py b/roborock/data/code_mappings.py index a08644080..6c066193a 100644 --- a/roborock/data/code_mappings.py +++ b/roborock/data/code_mappings.py @@ -10,10 +10,22 @@ class RoborockEnum(IntEnum): """Roborock Enum for codes with int values""" + _display_name_: str | None + + def __new__(cls, value: int, display_name: str | None = None) -> Self: + member = int.__new__(cls, value) + member._value_ = value + member._display_name_ = display_name + return member + @property def name(self) -> str: return super().name.lower() + @property + def display_name(self) -> str: + return self._display_name_ or self.name + @classmethod def _missing_(cls: type[Self], key) -> Self: if hasattr(cls, "unknown"): @@ -30,8 +42,13 @@ def _missing_(cls: type[Self], key) -> Self: return default_value @classmethod - def as_dict(cls: type[Self]): - return {i.name: i.value for i in cls if i.name != "missing"} + def as_dict(cls: type[Self]) -> dict[str, int]: + result: dict[str, int] = {} + for item in cls: + if item.name == "missing": + continue + result.setdefault(item.display_name, item.value) + return result @classmethod def as_enum_dict(cls: type[Self]): diff --git a/roborock/data/dyad/dyad_code_mappings.py b/roborock/data/dyad/dyad_code_mappings.py index a1b396afd..ec12fdb2a 100644 --- a/roborock/data/dyad/dyad_code_mappings.py +++ b/roborock/data/dyad/dyad_code_mappings.py @@ -95,7 +95,7 @@ class DyadError(RoborockEnum): battery_temperature_protection = ( 20007 # Battery temperature protection. Wait for the temperature to return to a normal range. ) - battery_temperature_protection_2 = 20008 + battery_temperature_protection_2 = (20008, "battery_temperature_protection") power_adapter_error = 20009 # Check if the power adapter is working properly. dirty_charging_contacts = 10007 # Disconnection between the device and dock. Wipe charging contacts. low_battery = 20017 # Low battery level. Charge before starting self-cleaning. diff --git a/roborock/data/v1/v1_code_mappings.py b/roborock/data/v1/v1_code_mappings.py index 730be40ee..921e680f7 100644 --- a/roborock/data/v1/v1_code_mappings.py +++ b/roborock/data/v1/v1_code_mappings.py @@ -8,40 +8,40 @@ class RoborockFinishReason(RoborockEnum): manual_interrupt = 21 # Cleaning interrupted by user cleanup_interrupted = 24 # Cleanup interrupted manual_interrupt_2 = 21 - manual_interrupt_12 = 29 + manual_interrupt_12 = (29, "manual_interrupt") breakpoint = 32 # Could not continue cleaning - breakpoint_2 = 33 - cleanup_interrupted_2 = 34 - manual_interrupt_3 = 35 - manual_interrupt_4 = 36 - manual_interrupt_5 = 37 - manual_interrupt_6 = 43 + breakpoint_2 = (33, "breakpoint") + cleanup_interrupted_2 = (34, "cleanup_interrupted") + manual_interrupt_3 = (35, "manual_interrupt") + manual_interrupt_4 = (36, "manual_interrupt") + manual_interrupt_5 = (37, "manual_interrupt") + manual_interrupt_6 = (43, "manual_interrupt") locate_fail = 45 # Positioning Failed - cleanup_interrupted_3 = 64 - locate_fail_2 = 65 - manual_interrupt_7 = 48 - manual_interrupt_8 = 49 - manual_interrupt_9 = 50 - cleanup_interrupted_4 = 51 + cleanup_interrupted_3 = (64, "cleanup_interrupted") + locate_fail_2 = (65, "locate_fail") + manual_interrupt_7 = (48, "manual_interrupt") + manual_interrupt_8 = (49, "manual_interrupt") + manual_interrupt_9 = (50, "manual_interrupt") + cleanup_interrupted_4 = (51, "cleanup_interrupted") finished_cleaning = 52 # Finished cleaning - finished_cleaning_2 = 54 - finished_cleaning_3 = 55 - finished_cleaning_4 = 56 - finished_clenaing_5 = 57 - manual_interrupt_10 = 60 + finished_cleaning_2 = (54, "finished_cleaning") + finished_cleaning_3 = (55, "finished_cleaning") + finished_cleaning_4 = (56, "finished_cleaning") + finished_clenaing_5 = (57, "finished_cleaning") + manual_interrupt_10 = (60, "manual_interrupt") area_unreachable = 61 # Area unreachable - area_unreachable_2 = 62 + area_unreachable_2 = (62, "area_unreachable") washing_error = 67 # Washing error back_to_wash_failure = 68 # Failed to return to the dock - cleanup_interrupted_5 = 101 - breakpoint_4 = 102 - manual_interrupt_11 = 103 - cleanup_interrupted_6 = 104 - cleanup_interrupted_7 = 105 - cleanup_interrupted_8 = 106 - cleanup_interrupted_9 = 107 - cleanup_interrupted_10 = 109 - cleanup_interrupted_11 = 110 + cleanup_interrupted_5 = (101, "cleanup_interrupted") + breakpoint_4 = (102, "breakpoint") + manual_interrupt_11 = (103, "manual_interrupt") + cleanup_interrupted_6 = (104, "cleanup_interrupted") + cleanup_interrupted_7 = (105, "cleanup_interrupted") + cleanup_interrupted_8 = (106, "cleanup_interrupted") + cleanup_interrupted_9 = (107, "cleanup_interrupted") + cleanup_interrupted_10 = (109, "cleanup_interrupted") + cleanup_interrupted_11 = (110, "cleanup_interrupted") patrol_success = 114 # Cruise completed patrol_fail = 115 # Cruise failed pet_patrol_success = 116 # Pet found @@ -159,7 +159,7 @@ class ClearWaterBoxStatus(RoborockDssCodes): okay = 0 out_of_water = 1 - out_of_water_2 = 38 + out_of_water_2 = (38, "out_of_water") refill_error = 48 @@ -168,7 +168,7 @@ class DirtyWaterBoxStatus(RoborockDssCodes): okay = 0 full_not_installed = 1 - full_not_installed_2 = 39 + full_not_installed_2 = (39, "full_not_installed") drain_error = 49 @@ -232,7 +232,7 @@ class RoborockErrorCode(RoborockEnum): clear_brush_exception = 42 # Check that the water filter has been correctly installed clear_brush_exception_2 = 43 # Positioning button error filter_screen_exception = 44 # Clean the dock water filter - mopping_roller_2 = 45 # Wash roller may be jammed + mopping_roller_2 = (45, "mopping_roller_1") # Wash roller may be jammed up_water_exception = 48 drain_water_exception = 49 temperature_protection = 51 # Unit temperature protection @@ -701,7 +701,7 @@ class RoborockStateCode(RoborockEnum): segment_cleaning = 18 emptying_the_bin = 22 # on s7+ washing_the_mop = 23 # on a46 - washing_the_mop_2 = 25 + washing_the_mop_2 = (25, "washing_the_mop") going_to_wash_the_mop = 26 # on a46 in_call = 28 mapping = 29 diff --git a/roborock/data/v1/v1_containers.py b/roborock/data/v1/v1_containers.py index 46fb32912..61346d079 100644 --- a/roborock/data/v1/v1_containers.py +++ b/roborock/data/v1/v1_containers.py @@ -191,15 +191,15 @@ def square_meter_clean_area(self) -> float | None: @property def error_code_name(self) -> str | None: - return self.error_code.name if self.error_code is not None else None + return self.error_code.display_name if self.error_code is not None else None @property def state_name(self) -> str | None: - return self.state.name if self.state is not None else None + return self.state.display_name if self.state is not None else None @property def water_box_mode_name(self) -> str | None: - return self.water_box_mode.name if self.water_box_mode is not None else None + return self.water_box_mode.display_name if self.water_box_mode is not None else None @property def fan_power_options(self) -> list[str]: @@ -209,23 +209,23 @@ def fan_power_options(self) -> list[str]: @property def fan_power_name(self) -> str | None: - return self.fan_power.name if self.fan_power is not None else None + return self.fan_power.display_name if self.fan_power is not None else None @property def mop_mode_name(self) -> str | None: - return self.mop_mode.name if self.mop_mode is not None else None + return self.mop_mode.display_name if self.mop_mode is not None else None - def get_fan_speed_code(self, fan_speed: str) -> int: + def get_fan_speed_code(self, fan_speed: str) -> int | None: if self.fan_power is None: raise RoborockException("Attempted to get fan speed before status has been updated.") return self.fan_power.as_dict().get(fan_speed) - def get_mop_intensity_code(self, mop_intensity: str) -> int: + def get_mop_intensity_code(self, mop_intensity: str) -> int | None: if self.water_box_mode is None: raise RoborockException("Attempted to get mop_intensity before status has been updated.") return self.water_box_mode.as_dict().get(mop_intensity) - def get_mop_mode_code(self, mop_mode: str) -> int: + def get_mop_mode_code(self, mop_mode: str) -> int | None: if self.mop_mode is None: raise RoborockException("Attempted to get mop_mode before status has been updated.") return self.mop_mode.as_dict().get(mop_mode) @@ -361,11 +361,11 @@ def square_meter_clean_area(self) -> float | None: @property def error_code_name(self) -> str | None: - return self.error_code.name if self.error_code is not None else None + return self.error_code.display_name if self.error_code is not None else None @property def state_name(self) -> str | None: - return self.state.name if self.state is not None else None + return self.state.display_name if self.state is not None else None @property def current_map(self) -> int | None: diff --git a/tests/data/test_code_mappings.py b/tests/data/test_code_mappings.py index 48ef6f8e7..b93f296ba 100644 --- a/tests/data/test_code_mappings.py +++ b/tests/data/test_code_mappings.py @@ -9,6 +9,12 @@ from roborock import HomeDataProduct, RoborockCategory from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP, YXCleanType from roborock.data.code_mappings import completed_warnings +from roborock.data.dyad.dyad_code_mappings import DyadError +from roborock.data.v1.v1_code_mappings import ( + RoborockErrorCode, + RoborockFinishReason, + RoborockStateCode, +) def test_from_code() -> None: @@ -135,3 +141,22 @@ def test_yx_clean_type_from_value_readable_values(readable_value: str, expected_ def test_yx_clean_type_from_code_customized() -> None: """Test YXCleanType accepts custom mode code used by Q10 status updates.""" assert YXCleanType.from_code(4) is YXCleanType.CUSTOMIZED + + +def test_roborock_enum_display_names_group_duplicate_protocol_codes() -> None: + """Duplicate protocol meanings keep raw codes but expose one user-facing name.""" + assert RoborockStateCode(25) is RoborockStateCode.washing_the_mop_2 + assert RoborockStateCode(25).value == 25 + assert RoborockStateCode(25).display_name == "washing_the_mop" + assert RoborockStateCode.keys().count("washing_the_mop") == 1 + assert "washing_the_mop_2" not in RoborockStateCode.keys() + + assert RoborockErrorCode(45).display_name == "mopping_roller_1" + assert RoborockErrorCode.keys().count("mopping_roller_1") == 1 + assert "mopping_roller_2" not in RoborockErrorCode.keys() + + assert RoborockFinishReason(56).display_name == "finished_cleaning" + assert "finished_cleaning_4" not in RoborockFinishReason.keys() + + assert DyadError(20008).display_name == "battery_temperature_protection" + assert "battery_temperature_protection_2" not in DyadError.keys() diff --git a/tests/data/v1/test_v1_containers.py b/tests/data/v1/test_v1_containers.py index f8625eb35..3cdbe6f1d 100644 --- a/tests/data/v1/test_v1_containers.py +++ b/tests/data/v1/test_v1_containers.py @@ -199,6 +199,20 @@ def test_status_v2() -> None: assert s.current_map == 0 +def test_status_v2_uses_canonical_state_and_error_names() -> None: + """Status names should expose canonical user-facing strings.""" + modified_status = STATUS.copy() + modified_status["state"] = RoborockStateCode.washing_the_mop_2.value + modified_status["error_code"] = RoborockErrorCode.mopping_roller_2.value + + s = StatusV2.from_dict(modified_status) + + assert s.state == RoborockStateCode.washing_the_mop_2 + assert s.state_name == "washing_the_mop" + assert s.error_code == RoborockErrorCode.mopping_roller_2 + assert s.error_code_name == "mopping_roller_1" + + def test_status_v2_current_map() -> None: """Test the current map logic based on map status for StatusV2.""" s = StatusV2.from_dict(STATUS)