Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions roborock/data/code_mappings.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,14 +72,21 @@ class RoborockModeEnum(StrEnum):

code: int
"""The integer code associated with the enum member."""
_display_name_: str | None

def __new__(cls, value: str, code: int) -> Self:
def __new__(cls, value: str, code: int, display_name: str | None = None) -> Self:
"""Creates a new enum member."""
member = str.__new__(cls, value)
member._value_ = value
member.code = code
member._display_name_ = display_name
return member

@property
def display_name(self) -> str:
"""Return the canonical user-facing name for the mode."""
return self._display_name_ or self.value

@classmethod
def from_code(cls, code: int) -> Self:
for member in cls:
Expand DownExpand Up@@ -149,8 +156,8 @@ def from_any_optional(cls, value: str | int) -> Self | None:

@classmethod
def keys(cls) -> list[str]:
"""Returns a list of all member values."""
return [member.value for member in cls]
"""Returns a de-duplicated list of canonical member names."""
return list(dict.fromkeys(member.display_name for member in cls))

def __eq__(self, other: Any) -> bool:
if isinstance(other, str):
Expand Down
13 changes: 8 additions & 5 deletions roborock/data/v1/v1_clean_modes.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@ class CleanRoutes(RoborockModeEnum):
DEEP = ("deep", 301)
DEEP_PLUS = ("deep_plus", 303)
FAST = ("fast", 304)
DEEP_PLUS_CN = ("deep_plus", 305)
DEEP_PLUS_CN = ("deep_plus_cn", 305, "deep_plus")
SMART_MODE = ("smart_mode", 306)
CUSTOMIZED = ("custom", 302)

Expand DownExpand Up@@ -131,16 +131,19 @@ def get_clean_modes(features: DeviceFeatures) -> list[VacuumModes]:

def get_clean_routes(features: DeviceFeatures, region: str) -> list[CleanRoutes]:
"""The routes that the vacuum will take while mopping"""
if not features.is_clean_route_setting_supported:
return []
if features.is_none_pure_clean_mop_with_max_plus:
return [CleanRoutes.FAST, CleanRoutes.STANDARD]
supported = [CleanRoutes.STANDARD, CleanRoutes.DEEP]
supported = [CleanRoutes.STANDARD]
if not features.is_clean_efficiency_supported:
supported.append(CleanRoutes.DEEP)
if features.is_careful_slow_mop_supported:
if not (
features.is_corner_clean_mode_supported
if (
not features.is_corner_clean_mode_supported
and features.is_clean_route_deep_slow_plus_supported
and region == "cn"
):
# for some reason there is a china specific deep plus mode
supported.append(CleanRoutes.DEEP_PLUS_CN)
else:
supported.append(CleanRoutes.DEEP_PLUS)
Expand Down
8 changes: 8 additions & 0 deletions roborock/device_features.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,6 +87,7 @@ class NewFeatureStrBit(IntEnum):
TWO_GEARS_NO_COLLISION = 118
CARPET_SHAPE_TYPE = 119
SR_MAP = 120
ROLLER_MOP = 128


class ProductFeatures(StrEnum):
Expand DownExpand Up@@ -474,6 +475,7 @@ class DeviceFeatures(RoborockBase):
)
is_carpet_shape_type_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.CARPET_SHAPE_TYPE})
is_sr_map_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.SR_MAP})
is_roller_mop_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.ROLLER_MOP})

# Features from feature_info list
is_led_status_switch_supported: bool = field(metadata={"robot_features": 119})
Expand DownExpand Up@@ -644,6 +646,12 @@ def from_feature_flags(
if any(feat in available_features for feat in product_features): # type: ignore
kwargs[f.name] = True

# The app combines runtime shake-mop, model shake/spin, and roller-mop
# capabilities when deciding whether mop routes can be configured.
kwargs["is_clean_route_setting_supported"] |= (
kwargs["is_shake_mop_set_supported"] or kwargs["is_roller_mop_supported"]
)

return cls(**kwargs)

def get_supported_features(self) -> list[str]:
Expand Down
2 changes: 1 addition & 1 deletion roborock/devices/traits/v1/status.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,7 +87,7 @@ def mop_route_options(self) -> list[CleanRoutes]:

@cached_property
def mop_route_mapping(self) -> dict[int, str]:
return {route.code: route.value for route in self.mop_route_options}
return {route.code: route.display_name for route in self.mop_route_options}

@cached_property
def cleaning_mode_options(self) -> list[CleaningMode]:
Expand Down
11 changes: 10 additions & 1 deletion tests/__snapshots__/test_supported_features.ambr
Original file line numberDiff line numberDiff line change
Expand Up@@ -107,6 +107,7 @@
'is_record_allowed': True,
'is_remote_supported': False,
'is_right_brush_stretch_supported': False,
'is_roller_mop_supported': False,
'is_room_name_supported': True,
'is_rpc_retry_supported': True,
'is_rubber_brush_carpet_supported': False,
Expand DownExpand Up@@ -285,6 +286,7 @@
'is_record_allowed': False,
'is_remote_supported': False,
'is_right_brush_stretch_supported': False,
'is_roller_mop_supported': False,
'is_room_name_supported': False,
'is_rpc_retry_supported': False,
'is_rubber_brush_carpet_supported': False,
Expand DownExpand Up@@ -386,7 +388,7 @@
'is_clean_history_time_line_supported': False,
'is_clean_route_deep_slow_plus_supported': False,
'is_clean_route_fast_mode_supported': True,
'is_clean_route_setting_supported': False,
'is_clean_route_setting_supported': True,
'is_clean_then_mop_mode_supported': False,
'is_clean_time_line_supported': False,
'is_collect_dust_count_show_supported': False,
Expand DownExpand Up@@ -463,6 +465,7 @@
'is_record_allowed': True,
'is_remote_supported': False,
'is_right_brush_stretch_supported': False,
'is_roller_mop_supported': False,
'is_room_name_supported': True,
'is_rpc_retry_supported': True,
'is_rubber_brush_carpet_supported': False,
Expand DownExpand Up@@ -641,6 +644,7 @@
'is_record_allowed': False,
'is_remote_supported': False,
'is_right_brush_stretch_supported': False,
'is_roller_mop_supported': False,
'is_room_name_supported': False,
'is_rpc_retry_supported': False,
'is_rubber_brush_carpet_supported': False,
Expand DownExpand Up@@ -819,6 +823,7 @@
'is_record_allowed': False,
'is_remote_supported': False,
'is_right_brush_stretch_supported': False,
'is_roller_mop_supported': False,
'is_room_name_supported': False,
'is_rpc_retry_supported': False,
'is_rubber_brush_carpet_supported': False,
Expand DownExpand Up@@ -997,6 +1002,7 @@
'is_record_allowed': True,
'is_remote_supported': False,
'is_right_brush_stretch_supported': False,
'is_roller_mop_supported': False,
'is_room_name_supported': True,
'is_rpc_retry_supported': True,
'is_rubber_brush_carpet_supported': False,
Expand DownExpand Up@@ -1175,6 +1181,7 @@
'is_record_allowed': True,
'is_remote_supported': False,
'is_right_brush_stretch_supported': True,
'is_roller_mop_supported': False,
'is_room_name_supported': True,
'is_rpc_retry_supported': True,
'is_rubber_brush_carpet_supported': False,
Expand DownExpand Up@@ -1353,6 +1360,7 @@
'is_record_allowed': True,
'is_remote_supported': False,
'is_right_brush_stretch_supported': True,
'is_roller_mop_supported': False,
'is_room_name_supported': True,
'is_rpc_retry_supported': True,
'is_rubber_brush_carpet_supported': False,
Expand DownExpand Up@@ -1531,6 +1539,7 @@
'is_record_allowed': False,
'is_remote_supported': False,
'is_right_brush_stretch_supported': False,
'is_roller_mop_supported': False,
'is_room_name_supported': False,
'is_rpc_retry_supported': False,
'is_rubber_brush_carpet_supported': False,
Expand Down
12 changes: 11 additions & 1 deletion tests/data/test_code_mappings.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@

import pytest

from roborock import HomeDataProduct, RoborockCategory
from roborock import CleanRoutes, 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
Expand DownExpand Up@@ -160,3 +160,13 @@ def test_roborock_enum_display_names_group_duplicate_protocol_codes() -> None:

assert DyadError(20008).display_name == "battery_temperature_protection"
assert "battery_temperature_protection_2" not in DyadError.keys()


def test_roborock_mode_enum_display_names_group_duplicate_protocol_codes() -> None:
"""Mode variants keep distinct codes but expose one translation key."""
assert CleanRoutes.DEEP_PLUS.code == 303
assert CleanRoutes.DEEP_PLUS_CN.code == 305
assert CleanRoutes.DEEP_PLUS_CN.value == "deep_plus_cn"
assert CleanRoutes.DEEP_PLUS_CN.display_name == "deep_plus"
assert CleanRoutes.keys().count("deep_plus") == 1
assert "deep_plus_cn" not in CleanRoutes.keys()
1 change: 1 addition & 0 deletions tests/devices/__snapshots__/test_device_manager.ambr
Original file line numberDiff line numberDiff line change
Expand Up@@ -289,6 +289,7 @@
'isRecordAllowed': False,
'isRemoteSupported': True,
'isRightBrushStretchSupported': False,
'isRollerMopSupported': False,
'isRoomNameSupported': True,
'isRpcRetrySupported': False,
'isRubberBrushCarpetSupported': False,
Expand Down
6 changes: 3 additions & 3 deletions tests/devices/__snapshots__/test_file_cache.ambr

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions tests/devices/__snapshots__/test_v1_device.ambr
Original file line numberDiff line numberDiff line change
Expand Up@@ -317,6 +317,7 @@
'isRecordAllowed': False,
'isRemoteSupported': True,
'isRightBrushStretchSupported': False,
'isRollerMopSupported': False,
'isRoomNameSupported': True,
'isRpcRetrySupported': False,
'isRubberBrushCarpetSupported': False,
Expand DownExpand Up@@ -737,6 +738,7 @@
'isRecordAllowed': False,
'isRemoteSupported': True,
'isRightBrushStretchSupported': False,
'isRollerMopSupported': False,
'isRoomNameSupported': True,
'isRpcRetrySupported': False,
'isRubberBrushCarpetSupported': False,
Expand DownExpand Up@@ -1164,6 +1166,7 @@
'isRecordAllowed': False,
'isRemoteSupported': True,
'isRightBrushStretchSupported': False,
'isRollerMopSupported': False,
'isRoomNameSupported': True,
'isRpcRetrySupported': False,
'isRubberBrushCarpetSupported': False,
Expand DownExpand Up@@ -1561,6 +1564,7 @@
'isRecordAllowed': False,
'isRemoteSupported': True,
'isRightBrushStretchSupported': False,
'isRollerMopSupported': False,
'isRoomNameSupported': True,
'isRpcRetrySupported': False,
'isRubberBrushCarpetSupported': False,
Expand Down
112 changes: 111 additions & 1 deletion tests/devices/traits/v1/test_status.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@
get_current_cleaning_mode,
resolve_cleaning_mode,
)
from roborock.data import SHORT_MODEL_TO_ENUM
from roborock.data import SHORT_MODEL_TO_ENUM, RoborockProductNickname
from roborock.data.v1 import (
RoborockStateCode,
)
Expand DownExpand Up@@ -110,6 +110,7 @@ def test_none_values(status_trait: StatusTrait) -> None:

def test_options(status_trait: StatusTrait) -> None:
"""Test that fan_speed_options returns a list of options."""
status_trait._device_features_trait.is_clean_route_setting_supported = True
assert isinstance(status_trait.fan_speed_options, list)
assert len(status_trait.fan_speed_options) > 0
assert isinstance(status_trait.water_mode_options, list)
Expand All@@ -118,6 +119,115 @@ def test_options(status_trait: StatusTrait) -> None:
assert len(status_trait.mop_route_options) > 0


def test_s6_maxv_has_no_mop_route_options() -> None:
"""Test S6 MaxV does not expose the unsupported mop-route selector."""
features = DeviceFeatures.from_feature_flags(
new_feature_info=10738169343,
new_feature_info_str="",
feature_info=[],
product_nickname=SHORT_MODEL_TO_ENUM["a10"],
)
status_trait = StatusTrait(cast(DeviceFeaturesTrait, features), region="us")

assert not features.is_shake_mop_set_supported
assert status_trait.mop_route_options == []
assert status_trait.mop_route_mapping == {}
assert get_cleaning_mode_parameters(CleaningMode.VAC_AND_MOP, features) == [
{
"fan_power": VacuumModes.BALANCED.code,
"water_box_mode": WaterModes.STANDARD.code,
}
]


def test_s7_has_mop_route_options() -> None:
"""Test S7 exposes its supported mop routes."""
features = DeviceFeatures.from_feature_flags(
new_feature_info=636084721975295,
new_feature_info_str="0000000000002000",
feature_info=[111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 122, 123, 124, 125],
product_nickname=SHORT_MODEL_TO_ENUM["a15"],
)
status_trait = StatusTrait(cast(DeviceFeaturesTrait, features), region="us")

assert features.is_shake_mop_set_supported
assert CleanRoutes.STANDARD in status_trait.mop_route_options
assert CleanRoutes.DEEP in status_trait.mop_route_options
assert get_cleaning_mode_parameters(CleaningMode.VAC_AND_MOP, features) == [
{
"fan_power": VacuumModes.BALANCED.code,
"water_box_mode": WaterModes.STANDARD.code,
"mop_mode": CleanRoutes.STANDARD.code,
}
]


def test_spin_mop_without_runtime_shake_has_mop_route_options() -> None:
"""Test spin-mop models retain routes without the runtime shake bit."""
features = DeviceFeatures.from_feature_flags(
new_feature_info=0,
new_feature_info_str="42BA8D587EDAFFFE",
feature_info=[],
product_nickname=SHORT_MODEL_TO_ENUM["a123"],
)
status_trait = StatusTrait(cast(DeviceFeaturesTrait, features), region="us")

assert not features.is_shake_mop_set_supported
assert features.is_clean_route_setting_supported
assert CleanRoutes.STANDARD in status_trait.mop_route_options
assert CleanRoutes.DEEP in status_trait.mop_route_options
assert get_cleaning_mode_parameters(CleaningMode.VAC_AND_MOP, features)[0]["mop_mode"] == 300


def test_clean_efficiency_device_omits_deep_route() -> None:
"""Test Saros 20-style clean-efficiency devices do not expose route 301."""
features = DeviceFeatures.from_feature_flags(
new_feature_info=4499197267967999,
new_feature_info_str="0000000000099518CCFF7EFDA8E93EDDDBFF8F7F7EFEFFFF",
feature_info=[111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125],
product_nickname=RoborockProductNickname.PEARLPLUS,
)
status_trait = StatusTrait(cast(DeviceFeaturesTrait, features), region="us")

assert features.is_clean_efficiency_supported
assert features.is_clean_route_setting_supported
assert CleanRoutes.STANDARD in status_trait.mop_route_options
assert CleanRoutes.DEEP not in status_trait.mop_route_options
assert CleanRoutes.DEEP_PLUS in status_trait.mop_route_options
assert CleanRoutes.FAST in status_trait.mop_route_options


@pytest.mark.parametrize(
("is_corner_clean_mode_supported", "is_clean_route_deep_slow_plus_supported", "region", "expected_code"),
[
(False, False, "us", 303),
(False, True, "us", 303),
(True, True, "us", 303),
(False, False, "cn", 303),
(True, True, "cn", 303),
(False, True, "cn", 305),
],
)
def test_deep_plus_route_matches_rr_api(
is_corner_clean_mode_supported: bool,
is_clean_route_deep_slow_plus_supported: bool,
region: str,
expected_code: int,
) -> None:
"""Test route 305 is limited to its RR_API China-only feature combination."""
status_trait = _create_cleaning_mode_status_trait(
is_careful_slow_mop_supported=True,
is_corner_clean_mode_supported=is_corner_clean_mode_supported,
is_clean_route_deep_slow_plus_supported=is_clean_route_deep_slow_plus_supported,
)
status_trait._region = region

route_codes = [route.code for route in status_trait.mop_route_options]
assert expected_code in route_codes
assert ({303, 305} - {expected_code}).isdisjoint(route_codes)
assert status_trait.mop_route_mapping[expected_code] == "deep_plus"


def test_cleaning_mode_options() -> None:
"""Test the high-level cleaning mode options for the device."""
status_trait = _create_cleaning_mode_status_trait()
Expand Down
Loading
Loading