diff --git a/roborock/api.py b/roborock/api.py index ac11459d..6348dd39 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -20,7 +20,7 @@ from Crypto.Cipher import AES from Crypto.Util.Padding import unpad -from .code_mappings import RoborockDockTypeCode, RoborockEnum +from .code_mappings import RoborockDockTypeCode from .containers import ( CleanRecord, CleanSummary, @@ -166,7 +166,7 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None: def on_connection_lost(self, exc: Optional[Exception]) -> None: self._last_disconnection = self.time_func() - _LOGGER.warning("Roborock client disconnected") + _LOGGER.info("Roborock client disconnected") if exc is not None: _LOGGER.warning(exc) @@ -223,7 +223,10 @@ async def send_command(self, method: RoborockCommand, params: Optional[list | di async def get_status(self) -> Status | None: status = await self.send_command(RoborockCommand.GET_STATUS) if isinstance(status, dict): - return Status.from_dict(status) + status = Status.from_dict(status) + status.update_status(self.device_info.model_specification) + return status + return None async def get_dnd_timer(self) -> DNDTimer | None: @@ -294,7 +297,7 @@ async def get_smart_wash_params(self) -> SmartWashParams | None: _LOGGER.error(e) return None - async def get_dock_summary(self, dock_type: RoborockEnum) -> DockSummary | None: + async def get_dock_summary(self, dock_type: RoborockDockTypeCode) -> DockSummary | None: """Gets the status summary from the dock with the methods available for a given dock. :param dock_type: RoborockDockTypeCode""" @@ -306,7 +309,7 @@ async def get_dock_summary(self, dock_type: RoborockEnum) -> DockSummary | None: DustCollectionMode | WashTowelMode | SmartWashParams | None, ] ] = [self.get_dust_collection_mode()] - if dock_type == RoborockDockTypeCode["3"]: + if dock_type == RoborockDockTypeCode.empty_wash_fill_dock or dock_type == RoborockDockTypeCode.s8_dock: commands += [ self.get_wash_towel_mode(), self.get_smart_wash_params(), @@ -333,7 +336,7 @@ async def get_prop(self) -> DeviceProp | None: if clean_summary and clean_summary.records and len(clean_summary.records) > 0: last_clean_record = await self.get_clean_record(clean_summary.records[0]) dock_summary = None - if status and status.dock_type is not None and status.dock_type != RoborockDockTypeCode["0"]: + if status and status.dock_type is not None and status.dock_type != RoborockDockTypeCode.no_dock: dock_summary = await self.get_dock_summary(status.dock_type) if any([status, dnd_timer, clean_summary, consumable]): return DeviceProp( diff --git a/roborock/cli.py b/roborock/cli.py index 79625087..51d5ffd3 100644 --- a/roborock/cli.py +++ b/roborock/cli.py @@ -121,8 +121,12 @@ async def command(ctx, cmd, device_id, params): await _discover(ctx) login_data = context.login_data() home_data = login_data.home_data - device = next((device for device in home_data.get_all_devices() if device.duid == device_id)) - device_info = RoborockDeviceInfo(device=device) + devices = home_data.devices + home_data.received_devices + device = next((device for device in devices if device.duid == device_id), None) + model_specification = next( + (product.model_specification for product in home_data.products if product.did == device.duid), None + ) + device_info = RoborockDeviceInfo(device=device, model_specification=model_specification) mqtt_client = RoborockMqttClient(login_data.user_data, device_info) await mqtt_client.send_command(cmd, params) mqtt_client.__del__() diff --git a/roborock/code_mappings.py b/roborock/code_mappings.py index 9bdd3cb5..b50fa145 100644 --- a/roborock/code_mappings.py +++ b/roborock/code_mappings.py @@ -1,183 +1,320 @@ from __future__ import annotations import logging -from enum import Enum -from typing import Any, Type, TypeVar +from dataclasses import dataclass +from enum import IntEnum +from typing import Type -_StrEnumT = TypeVar("_StrEnumT", bound="RoborockEnum") +from roborock.const import ( + ROBOROCK_Q7_MAX, + ROBOROCK_S5_MAX, + ROBOROCK_S6_MAXV, + ROBOROCK_S6_PURE, + ROBOROCK_S7, + ROBOROCK_S7_MAXV, + ROBOROCK_S8_PRO_ULTRA, +) _LOGGER = logging.getLogger(__name__) -class RoborockEnum(str, Enum): - def __new__(cls: Type[_StrEnumT], value: str, *args: Any, **kwargs: Any) -> _StrEnumT: - """Create a new StrEnum instance.""" - if not isinstance(value, str): - raise TypeError(f"{value!r} is not a string") - return super().__new__(cls, value, *args, **kwargs) +class RoborockEnum(IntEnum): + """Roborock Enum for codes with int values""" - def __str__(self): - return str(self.value) + @classmethod + def _missing_(cls: Type[RoborockEnum], key) -> str: + if hasattr(cls, "missing"): + _LOGGER.warning(f"Missing {cls.__name__} code: {key} - defaulting to 'missing'") + return cls.missing # type: ignore + _LOGGER.warning(f"Missing {cls.__name__} code: {key} - defaulting to {cls.keys()[0]}") + return cls.keys()[0] @classmethod - def _missing_(cls: Type[_StrEnumT], code: object): - if cls._member_map_.get(str(code)): - return cls._member_map_.get(str(code)) - else: - _LOGGER.warning(f"Unknown code {code} for {cls.__name__}") - return cls._member_map_.get(str(-9999)) + def as_dict(cls: Type[RoborockEnum]): + return {i.value: i.name for i in cls if i.name != "missing"} @classmethod - def as_dict(cls: Type[_StrEnumT]): - return {int(i.name): i.value for i in cls if i.value != "UNKNOWN"} + def as_enum_dict(cls: Type[RoborockEnum]): + return {i.value: i for i in cls if i.name != "missing"} @classmethod - def values(cls: Type[_StrEnumT]): + def values(cls: Type[RoborockEnum]) -> list[int]: return list(cls.as_dict().values()) @classmethod - def keys(cls: Type[_StrEnumT]): + def keys(cls: Type[RoborockEnum]) -> list[str]: return list(cls.as_dict().keys()) @classmethod - def items(cls: Type[_StrEnumT]): + def items(cls: Type[RoborockEnum]): return cls.as_dict().items() - @classmethod - def __getitem__(cls: Type[_StrEnumT], item): - return cls.__getitem__(item) - - -def create_code_enum(name: str, data: dict) -> RoborockEnum: - data[-9999] = "UNKNOWN" - return RoborockEnum(name, {str(key): value for key, value in data.items()}) - - -RoborockStateCode = create_code_enum( - "RoborockStateCode", - { - 1: "starting", - 2: "charger_disconnected", - 3: "idle", - 4: "remote_control_active", - 5: "cleaning", - 6: "returning_home", - 7: "manual_mode", - 8: "charging", - 9: "charging_problem", - 10: "paused", - 11: "spot_cleaning", - 12: "error", - 13: "shutting_down", - 14: "updating", - 15: "docking", - 16: "going_to_target", - 17: "zoned_cleaning", - 18: "segment_cleaning", - 22: "emptying_the_bin", # on s7+, see #1189 - 23: "washing_the_mop", # on a46, #1435 - 26: "going_to_wash_the_mop", # on a46, #1435 - 100: "charging_complete", - 101: "device_offline", - }, -) -RoborockErrorCode = create_code_enum( - "RoborockErrorCode", - { - 0: "none", - 1: "lidar_blocked", - 2: "bumper_stuck", - 3: "wheels_suspended", - 4: "cliff_sensor_error", - 5: "main_brush_jammed", - 6: "side_brush_jammed", - 7: "wheels_jammed", - 8: "robot_trapped", - 9: "no_dustbin", - 12: "low_battery", - 13: "charging_error", - 14: "battery_error", - 15: "wall_sensor_dirty", - 16: "robot_tilted", - 17: "side_brush_error", - 18: "fan_error", - 21: "vertical_bumper_pressed", - 22: "dock_locator_error", - 23: "return_to_dock_fail", - 24: "nogo_zone_detected", - 27: "vibrarise_jammed", - 28: "robot_on_carpet", - 29: "filter_blocked", - 30: "invisible_wall_detected", - 31: "cannot_cross_carpet", - 32: "internal_error", - }, -) +class RoborockStateCode(RoborockEnum): + starting = 1 + charger_disconnected = 2 + idle = 3 + remote_control_active = 4 + cleaning = 5 + returning_home = 6 + manual_mode = 7 + charging = 8 + charging_problem = 9 + paused = 10 + spot_cleaning = 11 + error = 12 + shutting_down = 13 + updating = 14 + docking = 15 + going_to_target = 16 + zoned_cleaning = 17 + segment_cleaning = 18 + emptying_the_bin = 22 # on s7+ + washing_the_mop = 23 # on a46 + going_to_wash_the_mop = 26 # on a46 + charging_complete = 100 + device_offline = 101 -RoborockFanPowerCode = create_code_enum( - "RoborockFanPowerCode", - { - 105: "off", - 101: "silent", - 102: "balanced", - 103: "turbo", - 104: "max", - 108: "max_plus", - 106: "custom", - }, -) -RoborockMopModeCode = create_code_enum( - "RoborockMopModeCode", - { - 300: "standard", - 301: "deep", - 303: "deep_plus", - 302: "custom", - }, -) +class RoborockErrorCode(RoborockEnum): + none = 0 + lidar_blocked = 1 + bumper_stuck = 2 + wheels_suspended = 3 + cliff_sensor_error = 4 + main_brush_jammed = 5 + side_brush_jammed = 6 + wheels_jammed = 7 + robot_trapped = 8 + no_dustbin = 9 + low_battery = 12 + charging_error = 13 + battery_error = 14 + wall_sensor_dirty = 15 + robot_tilted = 16 + side_brush_error = 17 + fan_error = 18 + vertical_bumper_pressed = 21 + dock_locator_error = 22 + return_to_dock_fail = 23 + nogo_zone_detected = 24 + vibrarise_jammed = 27 + robot_on_carpet = 28 + filter_blocked = 29 + invisible_wall_detected = 30 + cannot_cross_carpet = 31 + internal_error = 32 -RoborockMopIntensityCode = create_code_enum( - "RoborockMopIntensityCode", - { - 200: "off", - 201: "mild", - 202: "moderate", - 203: "intense", - 204: "custom", - }, -) -RoborockDockErrorCode = create_code_enum( - "RoborockDockErrorCode", - { - 0: "ok", - 38: "water empty", - 39: "waste water tank full", - }, -) +class RoborockFanPowerCode(RoborockEnum): + """Describes the fan power of the vacuum cleaner.""" -RoborockDockTypeCode = create_code_enum( - "RoborockDockTypeCode", - {0: "no_dock", 1: "unknown", 2: "unknown", 3: "empty_wash_fill_dock", 4: "unknown", 5: "auto_empty_dock_pure"}, -) + # Fan speeds should have the first letter capitalized - as there is no way to change the name in translations as + # far as I am aware -RoborockDockDustCollectionModeCode = create_code_enum( - "RoborockDockDustCollectionModeCode", - { - 0: "smart", - 1: "light", - 2: "balanced", - 4: "max", - }, -) -RoborockDockWashTowelModeCode = create_code_enum( - "RoborockDockWashTowelModeCode", - { - 0: "light", - 1: "balanced", - 2: "deep", - }, -) +class RoborockFanSpeedV1(RoborockFanPowerCode): + Silent = 38 + Standard = 60 + Medium = 77 + Turbo = 90 + + +class RoborockFanSpeedV2(RoborockFanPowerCode): + Silent = 101 + Balanced = 102 + Turbo = 103 + Max = 104 + Gentle = 105 + Auto = 106 + + +class RoborockFanSpeedV3(RoborockFanPowerCode): + Silent = 38 + Standard = 60 + Medium = 75 + Turbo = 100 + + +class RoborockFanSpeedE2(RoborockFanPowerCode): + Gentle = 41 + Silent = 50 + Standard = 68 + Medium = 79 + Turbo = 100 + + +class RoborockFanSpeedS7(RoborockFanPowerCode): + Off = 105 + Quiet = 101 + Balanced = 102 + Turbo = 103 + Max = 104 + Custom = 106 + + +class RoborockFanSpeedS7MaxV(RoborockFanPowerCode): + Off = 105 + Quiet = 101 + Balanced = 102 + Turbo = 103 + Max = 104 + Max_plus = 108 + + +class RoborockFanSpeedS6Pure(RoborockFanPowerCode): + Gentle = 105 + Quiet = 101 + Balanced = 102 + Turbo = 103 + Max = 104 + + +class RoborockFanSpeedQ7Max(RoborockFanPowerCode): + Quiet = 101 + Balanced = 102 + Turbo = 103 + Max = 104 + + +class RoborockMopModeCode(RoborockEnum): + """Describes the mop mode of the vacuum cleaner.""" + + +class RoborockMopModeS7(RoborockMopModeCode): + """Describes the mop mode of the vacuum cleaner.""" + + standard = 300 + deep = 301 + custom = 302 + deep_plus = 303 + + +class RoborockMopModeS8ProUltra(RoborockMopModeCode): + standard = 300 + deep = 301 + deep_plus = 303 + fast = 304 + + +class RoborockMopIntensityCode(RoborockEnum): + """Describes the mop intensity of the vacuum cleaner.""" + + +class RoborockMopIntensityS7(RoborockMopIntensityCode): + """Describes the mop intensity of the vacuum cleaner.""" + + off = 200 + mild = 201 + moderate = 202 + intense = 203 + custom = 204 + + +class RoborockMopIntensityV2(RoborockMopIntensityCode): + """Describes the mop intensity of the vacuum cleaner.""" + + off = 200 + low = 201 + medium = 202 + high = 203 + custom = 207 + + +class RoborockDockErrorCode(RoborockEnum): + """Describes the error code of the dock.""" + + ok = 0 + water_empty = 38 + waste_water_tank_full = 39 + + +class RoborockDockTypeCode(RoborockEnum): + missing = -9999 + no_dock = 0 + empty_wash_fill_dock = 3 + auto_empty_dock_pure = 5 + s8_dock = 7 + + +class RoborockDockDustCollectionModeCode(RoborockEnum): + """Describes the dust collection mode of the vacuum cleaner.""" + + # TODO: Get the correct values for various different docks + missing = -9999 + smart = 0 + light = 1 + balanced = 2 + max = 4 + + +class RoborockDockWashTowelModeCode(RoborockEnum): + """Describes the wash towel mode of the vacuum cleaner.""" + + # TODO: Get the correct values for various different docks + missing = -9999 + light = 0 + balanced = 1 + deep = 2 + + +@dataclass +class ModelSpecification: + model_name: str + model_code: str + fan_power_code: Type[RoborockFanPowerCode] + mop_mode_code: Type[RoborockMopModeCode] | None + mop_intensity_code: Type[RoborockMopIntensityCode] | None + + +model_specifications = { + ROBOROCK_S5_MAX: ModelSpecification( + model_name="Roborock S5 Max", + model_code=ROBOROCK_S5_MAX, + fan_power_code=RoborockFanSpeedS6Pure, + mop_mode_code=None, + mop_intensity_code=RoborockMopIntensityV2, + ), + ROBOROCK_Q7_MAX: ModelSpecification( + model_name="Roborock Q7 Max", + model_code=ROBOROCK_Q7_MAX, + fan_power_code=RoborockFanSpeedQ7Max, + mop_mode_code=None, + mop_intensity_code=RoborockMopIntensityV2, + ), + ROBOROCK_S6_MAXV: ModelSpecification( + model_name="Roborock S6 MaxV", + model_code=ROBOROCK_S6_MAXV, + fan_power_code=RoborockFanSpeedE2, + mop_mode_code=None, + mop_intensity_code=RoborockMopIntensityV2, + ), + ROBOROCK_S6_PURE: ModelSpecification( + model_name="Roborock S6 Pure", + model_code=ROBOROCK_S6_PURE, + fan_power_code=RoborockFanSpeedS6Pure, + mop_mode_code=None, + mop_intensity_code=None, + ), + ROBOROCK_S7_MAXV: ModelSpecification( + model_name="Roborock S7 MaxV", + model_code=ROBOROCK_S7_MAXV, + fan_power_code=RoborockFanSpeedS7MaxV, + mop_mode_code=RoborockMopModeS7, + mop_intensity_code=RoborockMopIntensityS7, + ), + ROBOROCK_S7: ModelSpecification( + model_name="Roborock S7", + model_code=ROBOROCK_S7, + fan_power_code=RoborockFanSpeedS7, + mop_mode_code=RoborockMopModeS7, + mop_intensity_code=RoborockMopIntensityS7, + ), + ROBOROCK_S8_PRO_ULTRA: ModelSpecification( + model_name="Roborock S8 Pro Ultra", + model_code=ROBOROCK_S8_PRO_ULTRA, + fan_power_code=RoborockFanSpeedS7MaxV, + mop_mode_code=RoborockMopModeS8ProUltra, + mop_intensity_code=RoborockMopIntensityS7, + ), +} diff --git a/roborock/const.py b/roborock/const.py index a2f84f2f..5df7f17c 100644 --- a/roborock/const.py +++ b/roborock/const.py @@ -3,3 +3,52 @@ SIDE_BRUSH_REPLACE_TIME = 720000 FILTER_REPLACE_TIME = 540000 SENSOR_DIRTY_REPLACE_TIME = 108000 + + +ROBOROCK_V1 = "ROBOROCK.vacuum.v1" +ROBOROCK_S4 = "roborock.vacuum.s4" +ROBOROCK_S4_MAX = "roborock.vacuum.a19" +ROBOROCK_S5 = "roborock.vacuum.s5" +ROBOROCK_S5_MAX = "roborock.vacuum.s5e" +ROBOROCK_S6 = "roborock.vacuum.s6" +ROBOROCK_T6 = "roborock.vacuum.t6" # cn s6 +ROBOROCK_E4 = "roborock.vacuum.a01" +ROBOROCK_S6_PURE = "roborock.vacuum.a08" +ROBOROCK_T7 = "roborock.vacuum.a11" # cn s7 +ROBOROCK_T7S = "roborock.vacuum.a14" +ROBOROCK_T7SPLUS = "roborock.vacuum.a23" +ROBOROCK_S7_MAXV = "roborock.vacuum.a27" +ROBOROCK_S7_PRO_ULTRA = "roborock.vacuum.a62" +ROBOROCK_Q5 = "roborock.vacuum.a34" +ROBOROCK_Q7 = "roborock.vacuum.a37" # CHECK THIS +ROBOROCK_Q7_MAX = "roborock.vacuum.a38" +ROBOROCK_Q7PLUS = "roborock.vacuum.a40" +ROBOROCK_G10S = "roborock.vacuum.a46" +ROBOROCK_G10 = "roborock.vacuum.a29" +ROBOROCK_S7 = "roborock.vacuum.a15" +ROBOROCK_S6_MAXV = "roborock.vacuum.a10" +ROBOROCK_E2 = "roborock.vacuum.e2" +ROBOROCK_1S = "roborock.vacuum.m1s" +ROBOROCK_C1 = "roborock.vacuum.c1" +ROBOROCK_S8_PRO_ULTRA = "roborock.vacuum.a61" # CHECK THIS +ROBOROCK_S8 = "roborock.vacuum.a60" # CHECK THIS +ROBOROCK_WILD = "roborock.vacuum.*" # wildcard + +SUPPORTED_VACUUMS = ( + [ # These are the devices that show up when you add a device - more could be supported and just not show up + ROBOROCK_G10, + ROBOROCK_Q5, + ROBOROCK_Q7, + ROBOROCK_Q7_MAX, + ROBOROCK_S4, + ROBOROCK_S5_MAX, + ROBOROCK_S6, + ROBOROCK_S6_MAXV, + ROBOROCK_S6_PURE, + ROBOROCK_S7_MAXV, + ROBOROCK_S8_PRO_ULTRA, + ROBOROCK_S8, + ROBOROCK_S4_MAX, + ROBOROCK_S7, + ] +) diff --git a/roborock/containers.py b/roborock/containers.py index 61235c0e..26a152be 100644 --- a/roborock/containers.py +++ b/roborock/containers.py @@ -1,13 +1,15 @@ from __future__ import annotations +import logging import re from dataclasses import asdict, dataclass from enum import Enum -from typing import Any, Optional +from typing import Any, Optional, Type from dacite import Config, from_dict from .code_mappings import ( + ModelSpecification, RoborockDockDustCollectionModeCode, RoborockDockErrorCode, RoborockDockTypeCode, @@ -17,8 +19,17 @@ RoborockMopIntensityCode, RoborockMopModeCode, RoborockStateCode, + model_specifications, ) -from .const import FILTER_REPLACE_TIME, MAIN_BRUSH_REPLACE_TIME, SENSOR_DIRTY_REPLACE_TIME, SIDE_BRUSH_REPLACE_TIME +from .const import ( + FILTER_REPLACE_TIME, + MAIN_BRUSH_REPLACE_TIME, + ROBOROCK_S7_MAXV, + SENSOR_DIRTY_REPLACE_TIME, + SIDE_BRUSH_REPLACE_TIME, +) + +_LOGGER = logging.getLogger(__name__) def camelize(s: str): @@ -115,6 +126,14 @@ class HomeDataProduct(RoborockBase): capability: Optional[int] = None category: Optional[str] = None schema: Optional[list[HomeDataProductSchema]] = None + model_specification: ModelSpecification | None = None + + def __post_init__(self): + if self.model not in model_specifications: + _LOGGER.warning("We don't have specific device information for your model, please open an issue.") + self.model_specification = model_specifications.get(ROBOROCK_S7_MAXV) + else: + self.model_specification = model_specifications.get(self.model) @dataclass @@ -122,12 +141,13 @@ class HomeDataDeviceStatus(RoborockBase): id: Optional[Any] = None name: Optional[Any] = None code: Optional[Any] = None - model: Optional[Any] = None + model: Optional[str] = None icon_url: Optional[Any] = None attribute: Optional[Any] = None capability: Optional[Any] = None category: Optional[Any] = None schema: Optional[Any] = None + model_specification: ModelSpecification | None = None @dataclass @@ -197,11 +217,11 @@ class LoginData(RoborockBase): class Status(RoborockBase): msg_ver: Optional[int] = None msg_seq: Optional[int] = None - state: Optional[RoborockStateCode] = None # type: ignore[valid-type] + state: Optional[RoborockStateCode] = None battery: Optional[int] = None clean_time: Optional[int] = None clean_area: Optional[int] = None - error_code: Optional[RoborockErrorCode] = None # type: ignore[valid-type] + error_code: Optional[RoborockErrorCode] = None map_present: Optional[int] = None in_cleaning: Optional[int] = None in_returning: Optional[int] = None @@ -211,13 +231,14 @@ class Status(RoborockBase): back_type: Optional[int] = None wash_phase: Optional[int] = None wash_ready: Optional[int] = None - fan_power: Optional[RoborockFanPowerCode] = None # type: ignore[valid-type] + fan_power: Optional[int] = None + fan_power_enum: Optional[Type[RoborockFanPowerCode]] = None dnd_enabled: Optional[int] = None map_status: Optional[int] = None is_locating: Optional[int] = None lock_status: Optional[int] = None - water_box_mode: Optional[RoborockMopIntensityCode] = None # type: ignore[valid-type] - mop_intensity: Optional[str] = None + water_box_mode: Optional[int] = None + water_box_mode_enum: Optional[Type[RoborockMopIntensityCode]] = None water_box_carriage_status: Optional[int] = None mop_forbidden_enable: Optional[int] = None camera_status: Optional[int] = None @@ -226,19 +247,27 @@ class Status(RoborockBase): home_sec_enable_password: Optional[int] = None adbumper_status: Optional[list[int]] = None water_shortage_status: Optional[int] = None - dock_type: Optional[RoborockDockTypeCode] = None # type: ignore[valid-type] + dock_type: Optional[RoborockDockTypeCode] = None dust_collection_status: Optional[int] = None auto_dust_collection: Optional[int] = None avoid_count: Optional[int] = None - mop_mode: Optional[RoborockMopModeCode] = None # type: ignore[valid-type] + mop_mode: Optional[int] = None + mop_mode_enum: Optional[Type[RoborockMopModeCode]] = None debug_mode: Optional[int] = None collision_avoid_status: Optional[int] = None switch_map_mode: Optional[int] = None - dock_error_status: Optional[RoborockDockErrorCode] = None # type: ignore[valid-type] + dock_error_status: Optional[RoborockDockErrorCode] = None charge_status: Optional[int] = None unsave_map_reason: Optional[int] = None unsave_map_flag: Optional[int] = None + def update_status(self, model_specification: ModelSpecification) -> None: + self.fan_power_enum = model_specification.fan_power_code.as_enum_dict()[self.fan_power] + if model_specification.mop_mode_code is not None: + self.mop_mode_enum = model_specification.mop_mode_code.as_enum_dict()[self.mop_mode] + if model_specification.mop_intensity_code is not None: + self.water_box_mode_enum = model_specification.mop_intensity_code.as_enum_dict()[self.water_box_mode] + @dataclass class DNDTimer(RoborockBase): @@ -336,12 +365,12 @@ class SmartWashParams(RoborockBase): @dataclass class DustCollectionMode(RoborockBase): - mode: Optional[RoborockDockDustCollectionModeCode] = None # type: ignore[valid-type] + mode: Optional[RoborockDockDustCollectionModeCode] = None @dataclass class WashTowelMode(RoborockBase): - wash_mode: Optional[RoborockDockWashTowelModeCode] = None # type: ignore[valid-type] + wash_mode: Optional[RoborockDockWashTowelModeCode] = None @dataclass @@ -356,6 +385,7 @@ class NetworkInfo(RoborockBase): @dataclass class RoborockDeviceInfo(RoborockBase): device: HomeDataDevice + model_specification: ModelSpecification @dataclass diff --git a/roborock/roborock_typing.py b/roborock/roborock_typing.py index cc834315..a43fc444 100644 --- a/roborock/roborock_typing.py +++ b/roborock/roborock_typing.py @@ -275,7 +275,7 @@ class CommandInfo: RoborockCommand.SET_CUSTOMIZE_CLEAN_MODE: CommandInfo( prefix=b"\x00\x00\x00\xa7", params={"data": [], "need_retry": 1} ), - RoborockCommand.SET_CUSTOM_MODE: CommandInfo(prefix=b"\x00\x00\x00\x87", params=[108]), + RoborockCommand.SET_CUSTOM_MODE: CommandInfo(prefix=b"\x00\x00\x00w", params=[108]), RoborockCommand.SET_DND_TIMER: CommandInfo(prefix=b"\x00\x00\x00\x87", params=[22, 0, 8, 0]), RoborockCommand.SET_DUST_COLLECTION_MODE: CommandInfo(prefix=b"\x00\x00\x00\x87", params=None), RoborockCommand.SET_FDS_ENDPOINT: CommandInfo(prefix=b"\x00\x00\x00\x97", params=["awsusor0.fds.api.xiaomi.com"]), diff --git a/tests/conftest.py b/tests/conftest.py index d5cb5a54..7d55bdfe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,9 @@ def mqtt_client(): user_data = UserData.from_dict(USER_DATA) home_data = HomeData.from_dict(HOME_DATA_RAW) - device_info = RoborockDeviceInfo(device=home_data.devices[0]) + device_info = RoborockDeviceInfo( + device=home_data.devices[0], model_specification=home_data.products[0].model_specification + ) client = RoborockMqttClient(user_data, device_info) yield client # Clean up any resources after the test diff --git a/tests/test_api.py b/tests/test_api.py index 99c10a0a..7eb0ab5b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -3,11 +3,17 @@ import paho.mqtt.client as mqtt import pytest -from roborock import HomeData, RoborockDockDustCollectionModeCode, RoborockDockWashTowelModeCode, UserData +from roborock import ( + HomeData, + RoborockDockDustCollectionModeCode, + RoborockDockTypeCode, + RoborockDockWashTowelModeCode, + UserData, +) from roborock.api import PreparedRequest, RoborockApiClient from roborock.cloud_api import RoborockMqttClient -from roborock.containers import RoborockDeviceInfo -from tests.mock_data import BASE_URL_REQUEST, GET_CODE_RESPONSE, HOME_DATA_RAW, USER_DATA +from roborock.containers import RoborockDeviceInfo, Status +from tests.mock_data import BASE_URL_REQUEST, GET_CODE_RESPONSE, HOME_DATA_RAW, STATUS, USER_DATA def test_can_create_roborock_client(): @@ -20,7 +26,9 @@ def test_can_create_prepared_request(): def test_can_create_mqtt_roborock(): home_data = HomeData.from_dict(HOME_DATA_RAW) - device_info = RoborockDeviceInfo(device=home_data.devices[0]) + device_info = RoborockDeviceInfo( + device=home_data.devices[0], model_specification=home_data.products[0].model_specification + ) RoborockMqttClient(UserData.from_dict(USER_DATA), device_info) @@ -69,19 +77,23 @@ async def test_get_home_data(): @pytest.mark.asyncio async def test_get_dust_collection_mode(): home_data = HomeData.from_dict(HOME_DATA_RAW) - device_info = RoborockDeviceInfo(device=home_data.devices[0]) + device_info = RoborockDeviceInfo( + device=home_data.devices[0], model_specification=home_data.products[0].model_specification + ) rmc = RoborockMqttClient(UserData.from_dict(USER_DATA), device_info) with patch("roborock.cloud_api.RoborockMqttClient.send_command") as command: command.return_value = {"mode": 1} dust = await rmc.get_dust_collection_mode() assert dust is not None - assert dust.mode == RoborockDockDustCollectionModeCode["1"] + assert dust.mode == RoborockDockDustCollectionModeCode.light @pytest.mark.asyncio async def test_get_mop_wash_mode(): home_data = HomeData.from_dict(HOME_DATA_RAW) - device_info = RoborockDeviceInfo(device=home_data.devices[0]) + device_info = RoborockDeviceInfo( + device=home_data.devices[0], model_specification=home_data.products[0].model_specification + ) rmc = RoborockMqttClient(UserData.from_dict(USER_DATA), device_info) with patch("roborock.cloud_api.RoborockMqttClient.send_command") as command: command.return_value = {"smart_wash": 0, "wash_interval": 1500} @@ -94,10 +106,36 @@ async def test_get_mop_wash_mode(): @pytest.mark.asyncio async def test_get_washing_mode(): home_data = HomeData.from_dict(HOME_DATA_RAW) - device_info = RoborockDeviceInfo(device=home_data.devices[0]) + device_info = RoborockDeviceInfo( + device=home_data.devices[0], model_specification=home_data.products[0].model_specification + ) rmc = RoborockMqttClient(UserData.from_dict(USER_DATA), device_info) with patch("roborock.cloud_api.RoborockMqttClient.send_command") as command: command.return_value = {"wash_mode": 2} washing_mode = await rmc.get_wash_towel_mode() assert washing_mode is not None - assert washing_mode.wash_mode == RoborockDockWashTowelModeCode["2"] + assert washing_mode.wash_mode == RoborockDockWashTowelModeCode.deep + assert washing_mode.wash_mode == 2 + + +@pytest.mark.asyncio +async def test_get_prop(): + home_data = HomeData.from_dict(HOME_DATA_RAW) + device_info = RoborockDeviceInfo( + device=home_data.devices[0], model_specification=home_data.products[0].model_specification + ) + rmc = RoborockMqttClient(UserData.from_dict(USER_DATA), device_info) + with patch("roborock.cloud_api.RoborockMqttClient.get_status") as get_status, patch( + "roborock.cloud_api.RoborockMqttClient.send_command" + ), patch("roborock.cloud_api.RoborockMqttClient.get_dust_collection_mode"): + status = Status.from_dict(STATUS) + status.update_status(home_data.products[0].model_specification) + status.dock_type = RoborockDockTypeCode.auto_empty_dock_pure + get_status.return_value = status + + props = await rmc.get_prop() + assert props + assert props.dock_summary + assert props.dock_summary.wash_towel_mode is None + assert props.dock_summary.smart_wash_params is None + assert props.dock_summary.dust_collection_mode is not None diff --git a/tests/test_containers.py b/tests/test_containers.py index 0b82c0cb..e91dc59d 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -1,12 +1,13 @@ -from roborock import CleanRecord, CleanSummary, Consumable, DNDTimer, HomeData, Status, UserData +from roborock import ROBOROCK_S7_MAXV, CleanRecord, CleanSummary, Consumable, DNDTimer, HomeData, Status, UserData from roborock.code_mappings import ( RoborockDockErrorCode, RoborockDockTypeCode, RoborockErrorCode, - RoborockFanPowerCode, - RoborockMopIntensityCode, - RoborockMopModeCode, + RoborockFanSpeedS7MaxV, + RoborockMopIntensityS7, + RoborockMopModeS7, RoborockStateCode, + model_specifications, ) from .mock_data import CLEAN_RECORD, CLEAN_SUMMARY, CONSUMABLE, DND_TIMER, HOME_DATA_RAW, STATUS, USER_DATA @@ -111,11 +112,11 @@ def test_status(): s = Status.from_dict(STATUS) assert s.msg_ver == 2 assert s.msg_seq == 458 - assert s.state == RoborockStateCode["8"] + assert s.state == RoborockStateCode.charging assert s.battery == 100 assert s.clean_time == 1176 assert s.clean_area == 20965000 - assert s.error_code == RoborockErrorCode["0"] + assert s.error_code == RoborockErrorCode.none assert s.map_present == 1 assert s.in_cleaning == 0 assert s.in_returning == 0 @@ -125,12 +126,12 @@ def test_status(): assert s.back_type == -1 assert s.wash_phase == 0 assert s.wash_ready == 0 - assert s.fan_power == RoborockFanPowerCode["102"] + assert s.fan_power == 102 assert s.dnd_enabled == 0 assert s.map_status == 3 assert s.is_locating == 0 assert s.lock_status == 0 - assert s.water_box_mode == RoborockMopIntensityCode["203"] + assert s.water_box_mode == 203 assert s.water_box_carriage_status == 1 assert s.mop_forbidden_enable == 1 assert s.camera_status == 3457 @@ -139,18 +140,22 @@ def test_status(): assert s.home_sec_enable_password == 0 assert s.adbumper_status == [0, 0, 0] assert s.water_shortage_status == 0 - assert s.dock_type == RoborockDockTypeCode["3"] + assert s.dock_type == RoborockDockTypeCode.empty_wash_fill_dock assert s.dust_collection_status == 0 assert s.auto_dust_collection == 1 assert s.avoid_count == 19 - assert s.mop_mode == RoborockMopModeCode["300"] + assert s.mop_mode == 300 assert s.debug_mode == 0 assert s.collision_avoid_status == 1 assert s.switch_map_mode == 0 - assert s.dock_error_status == RoborockDockErrorCode["0"] + assert s.dock_error_status == RoborockDockErrorCode.ok assert s.charge_status == 1 assert s.unsave_map_reason == 0 assert s.unsave_map_flag == 0 + s.update_status(model_specification=model_specifications[ROBOROCK_S7_MAXV]) + assert s.fan_power == RoborockFanSpeedS7MaxV.Balanced + assert s.mop_mode == RoborockMopModeS7.standard + assert s.water_box_mode == RoborockMopIntensityS7.intense def test_dnd_timer(): @@ -191,9 +196,8 @@ def test_clean_record(): def test_no_value(): modified_status = STATUS.copy() - modified_status["mop_mode"] = 9999 + modified_status["dock_type"] = 9999 s = Status.from_dict(modified_status) - - assert s.mop_mode == RoborockMopModeCode["-9999"] - assert "-9999" not in RoborockMopModeCode.keys() - assert "UNKNOWN" not in RoborockMopModeCode.values() + assert s.dock_type == RoborockDockTypeCode.missing + assert -9999 not in RoborockDockTypeCode.keys() + assert "missing" not in RoborockDockTypeCode.values()