diff --git a/docs/V1_DEVICE_FEATURES.md b/docs/V1_DEVICE_FEATURES.md index f9b0b9e1..90fc9694 100644 --- a/docs/V1_DEVICE_FEATURES.md +++ b/docs/V1_DEVICE_FEATURES.md @@ -12,6 +12,7 @@ This document provides comprehensive documentation for each `DeviceFeature` in t - [Robot Features (Array-based)](#robot-features-array-based) - [Model-Specific Features](#model-specific-features) - [Product Features](#product-features) +- [Dock Features](#dock-features) --- @@ -1432,3 +1433,58 @@ These features are determined by hardware capabilities and product variant. **Feature:** Customized Clean Settings **Description:** Advanced customization for mop-equipped models. **Impact:** Detailed control over mopping parameters. + +--- + +## Dock Features + +Dock capabilities are not part of `DeviceFeatures`. `RoborockDockFeatures.from_dock_type(dock_type, has_am)` derives +them from two status values: + +- `Status.dock_type` (`RoborockDockTypeCode`): the dock family. Unknown codes fall back to `unknown`, which counts as + no dock. +- `Status.has_am` (`(dss & 3) == 2`): whether the dock is the "A"/"AM" variant. The app lists these variants under + their own names (for example Shell 2SA, O5AM, K1SProA), but they report the same `dock_type` code as the base dock. + +Some codes have two member names. `type_27_dock` and `type_49_dock` are the original names and stay the canonical +ones; `shell_4_dock` (27) and `f1rs_dock` (49) are aliases with the app's dock names. + +| Code | Member | App dock names | +|---|---|---| +| 27 | `type_27_dock` (alias `shell_4_dock`) | Shell4, Shell4A | +| 48 | `f1rc_dock` | F1RC, F1RCA | +| 49 | `type_49_dock` (alias `f1rs_dock`) | F1RS, F1RSA | +| 50 | `f2rs_dock` | F2RS, F2RSA | + +The table lists the codes that are new or whose member name differs from the app's dock name. Every other member name +follows the app's dock name (for example `shell_3s_dock` is Shell 3S / Shell 3SA). + +Dock types: `has_dock`, `is_pure_collect`, `is_pure_wash`, `is_collect_wash`, `is_collect_wash_dry` and the derived +`is_collectable`, `is_washable` and `is_dryable`. Every dock type except no dock (O0, `unknown`), pure collect (O1, +OC), pure wash (O2) and collect-wash (O3) is a collect, wash and dry dock. + +Capabilities that depend on `has_am`: + +- `is_clean_fluid_auto_delivery_supported`: several Shell and K1 docks only in the AM variant. +- `is_clean_carousel_self_clean_supported`: only docks without AM. The F2R dock (43) is not included: the newest app + plugins no longer offer carousel self-cleaning for it. Older plugins still did, and earlier library versions + reported it as supported. +- `is_water_updown_drain_supported`: mostly the AM variant (it has the water drain module). +- `is_double_serial_communication_supported`: Shell 4RC and Shell 4D only in the AM variant. +- `is_multi_clean_fluid_supported`: Shell 4S without AM. + +Other capabilities depend only on the code: + +### is_no_support_hot_wash_towel +**Docks:** Shell E +**Description:** The dock cannot wash mops with hot water. The app hides hot mop washing for these docks even when +`DeviceFeatures.is_hot_wash_towel_supported` is true. + +### is_clean_carousel_supported +**Docks:** Pearl, Couple, Hera, Shell (except Shell 4R), K1 and F-series docks; none of the O-series docks. +**Description:** The dock has a carousel (wash tray strainer) that the app shows as a consumable. + +### is_special_support_wash_temp +**Docks:** Shell 2E Lite +**Description:** Dock part of the app's check of the same name. The app also checks the product: it is true for every +product except PearlEPro, which needs a Shell 2E Lite dock. diff --git a/roborock/data/v1/v1_code_mappings.py b/roborock/data/v1/v1_code_mappings.py index c5d5037f..50b70b44 100644 --- a/roborock/data/v1/v1_code_mappings.py +++ b/roborock/data/v1/v1_code_mappings.py @@ -342,6 +342,12 @@ class RoborockDockErrorCode(RoborockEnum): class RoborockDockTypeCode(RoborockEnum): + """V1 `dock_type` codes. + + The app's "A"/"AM" dock variants (for example Shell 2SA or O5AM) are not separate codes: they report the same + code as the base dock, and `Status.has_am` tells them apart. + """ + unknown = -9999 o0_dock = 0 o1_dock = 1 @@ -368,6 +374,7 @@ class RoborockDockTypeCode(RoborockEnum): hera_dock = 24 k1s_pro_dock = 26 type_27_dock = 27 + shell_4_dock = 27 # noqa: PIE796 # App name of code 27; alias of type_27_dock. k1c_lite_dock = 28 k1r_dock = 29 shell_2e_lite_dock = 30 @@ -386,7 +393,10 @@ class RoborockDockTypeCode(RoborockEnum): f1s_dock = 44 o7h_dock = 45 f1c_dock = 46 + f1rc_dock = 48 type_49_dock = 49 + f1rs_dock = 49 # noqa: PIE796 # App name of code 49; alias of type_49_dock. + f2rs_dock = 50 class RoborockDockDustCollectionModeCode(RoborockEnum): diff --git a/roborock/device_features.py b/roborock/device_features.py index 160d14cc..d9a3c496 100644 --- a/roborock/device_features.py +++ b/roborock/device_features.py @@ -719,6 +719,7 @@ def get_supported_features(self) -> list[str]: RoborockDockTypeCode.o7_dock, RoborockDockTypeCode.f1s_dock, RoborockDockTypeCode.o7h_dock, + RoborockDockTypeCode.k1s_dock, } _CLEAN_FLUID_AUTO_DELIVERY_AM_DOCK_TYPES = { @@ -726,7 +727,6 @@ def get_supported_features(self) -> list[str]: RoborockDockTypeCode.shell_3_dock, RoborockDockTypeCode.shell_2c_dock, RoborockDockTypeCode.k1_dock, - RoborockDockTypeCode.k1s_dock, RoborockDockTypeCode.shell_e_dock, RoborockDockTypeCode.shell_2e_dock, RoborockDockTypeCode.shell_3c_dock, @@ -769,9 +769,9 @@ def get_supported_features(self) -> list[str]: RoborockDockTypeCode.k1d_dock, RoborockDockTypeCode.shell_3p_dock, RoborockDockTypeCode.f1_dock, - RoborockDockTypeCode.f2r_dock, RoborockDockTypeCode.f1s_dock, RoborockDockTypeCode.f1c_dock, + RoborockDockTypeCode.f1rc_dock, } _WATER_UPDOWN_DRAIN_DOCK_TYPES = { @@ -805,10 +805,13 @@ def get_supported_features(self) -> list[str]: RoborockDockTypeCode.f1r_dock, RoborockDockTypeCode.f1s_dock, RoborockDockTypeCode.o7h_dock, + RoborockDockTypeCode.type_49_dock, + RoborockDockTypeCode.f2rs_dock, } _HATCH_DOOR_DOCK_COOL_FAN_DOCK_TYPES = { RoborockDockTypeCode.couple_dock, + RoborockDockTypeCode.hera_dock, } _SPECIAL_WASH_TEMP_DOCK_TYPES = { @@ -823,6 +826,9 @@ def get_supported_features(self) -> list[str]: RoborockDockTypeCode.f1r_dock, RoborockDockTypeCode.f2r_dock, RoborockDockTypeCode.f1c_dock, + RoborockDockTypeCode.f1rc_dock, + RoborockDockTypeCode.type_49_dock, + RoborockDockTypeCode.f2rs_dock, } _DOUBLE_SERIAL_COMMUNICATION_DOCK_TYPES = { @@ -841,6 +847,9 @@ def get_supported_features(self) -> list[str]: RoborockDockTypeCode.f1s_dock, RoborockDockTypeCode.o7h_dock, RoborockDockTypeCode.f1c_dock, + RoborockDockTypeCode.f1rc_dock, + RoborockDockTypeCode.type_49_dock, + RoborockDockTypeCode.f2rs_dock, } _DOUBLE_SERIAL_COMMUNICATION_AM_DOCK_TYPES = { @@ -852,6 +861,8 @@ def get_supported_features(self) -> list[str]: RoborockDockTypeCode.f1r_dock, RoborockDockTypeCode.f2r_dock, RoborockDockTypeCode.f1s_dock, + RoborockDockTypeCode.type_49_dock, + RoborockDockTypeCode.f2rs_dock, } _SIMPLE_WASH_MODE_DOCK_TYPES = { @@ -859,12 +870,60 @@ def get_supported_features(self) -> list[str]: RoborockDockTypeCode.f1r_dock, RoborockDockTypeCode.f2r_dock, RoborockDockTypeCode.f1c_dock, + RoborockDockTypeCode.f1rc_dock, + RoborockDockTypeCode.type_49_dock, + RoborockDockTypeCode.f2rs_dock, } _STEAM_WASH_DOCK_TYPES = { RoborockDockTypeCode.o7h_dock, } +_NO_HOT_WASH_TOWEL_DOCK_TYPES = { + RoborockDockTypeCode.shell_e_dock, +} + +_MULTI_CLEAN_FLUID_DOCK_TYPES = { + RoborockDockTypeCode.shell_4s_dock, +} + +_CLEAN_CAROUSEL_DOCK_TYPES = { + RoborockDockTypeCode.pearl_dock, + RoborockDockTypeCode.pearl_plus_dock, + RoborockDockTypeCode.shell_2s_dock, + RoborockDockTypeCode.couple_dock, + RoborockDockTypeCode.shell_3_dock, + RoborockDockTypeCode.shell_2c_dock, + RoborockDockTypeCode.shell_3s_dock, + RoborockDockTypeCode.k1_dock, + RoborockDockTypeCode.k1c_dock, + RoborockDockTypeCode.k1s_dock, + RoborockDockTypeCode.shell_e_dock, + RoborockDockTypeCode.shell_2e_dock, + RoborockDockTypeCode.shell_3c_dock, + RoborockDockTypeCode.hera_dock, + RoborockDockTypeCode.k1s_pro_dock, + RoborockDockTypeCode.type_27_dock, + RoborockDockTypeCode.k1c_lite_dock, + RoborockDockTypeCode.k1r_dock, + RoborockDockTypeCode.shell_2e_lite_dock, + RoborockDockTypeCode.shell_4rc_dock, + RoborockDockTypeCode.shell_4p_dock, + RoborockDockTypeCode.shell_4s_dock, + RoborockDockTypeCode.k1d_dock, + RoborockDockTypeCode.shell_3p_dock, + RoborockDockTypeCode.shell_4d_dock, + RoborockDockTypeCode.shell_2e_heat_dock, + RoborockDockTypeCode.f1_dock, + RoborockDockTypeCode.f1r_dock, + RoborockDockTypeCode.f2r_dock, + RoborockDockTypeCode.f1s_dock, + RoborockDockTypeCode.f1c_dock, + RoborockDockTypeCode.f1rc_dock, + RoborockDockTypeCode.type_49_dock, + RoborockDockTypeCode.f2rs_dock, +} + @dataclass(frozen=True) class RoborockDockFeatures: @@ -963,6 +1022,11 @@ def is_hatch_door_dock_cool_fan_supported(self) -> bool: @property def is_special_support_wash_temp(self) -> bool: + """Whether the dock is a Shell 2E Lite. + + The app's getter of the same name also depends on the product (true for every product except PearlEPro, + which needs a Shell 2E Lite dock). This dock-only property covers the dock part. + """ return self.dock_type in _SPECIAL_WASH_TEMP_DOCK_TYPES @property @@ -987,6 +1051,21 @@ def is_simple_wash_mode_supported(self) -> bool: def is_steam_wash_supported(self) -> bool: return self.dock_type in _STEAM_WASH_DOCK_TYPES + @property + def is_no_support_hot_wash_towel(self) -> bool: + """Whether the dock cannot wash mops with hot water, even if the robot reports hot mop washing.""" + return self.dock_type in _NO_HOT_WASH_TOWEL_DOCK_TYPES + + @property + def is_multi_clean_fluid_supported(self) -> bool: + """Whether the dock supports the app's multi cleaning fluid feature (Shell 4S without AM).""" + return self._matches_non_am_variant(_MULTI_CLEAN_FLUID_DOCK_TYPES) + + @property + def is_clean_carousel_supported(self) -> bool: + """Whether the dock has a carousel (wash tray strainer) that needs cleaning as a consumable.""" + return self.dock_type in _CLEAN_CAROUSEL_DOCK_TYPES + WASH_N_FILL_DOCK_TYPES = [ dock_type for dock_type in RoborockDockTypeCode if RoborockDockFeatures.from_dock_type(dock_type).is_washable diff --git a/tests/test_supported_features.py b/tests/test_supported_features.py index b427ccc5..ac34a00d 100644 --- a/tests/test_supported_features.py +++ b/tests/test_supported_features.py @@ -299,6 +299,8 @@ async def test_from_dict_rejects_legacy_missing_capabilities(caplog: pytest.LogC (RoborockDockTypeCode.o7h_dock, True, True), (RoborockDockTypeCode.f1c_dock, True, True), (RoborockDockTypeCode.type_49_dock, True, True), + (RoborockDockTypeCode.f1rc_dock, True, True), + (RoborockDockTypeCode.f2rs_dock, True, True), ], ) def test_dock_features(dock_type: RoborockDockTypeCode, is_collectable: bool, is_washable: bool) -> None: @@ -319,7 +321,8 @@ def test_all_rr_api_dock_types_are_mapped() -> None: if dock_type is not RoborockDockTypeCode.unknown } - observed_docks = {49: "type_49_dock"} # P30 Pro; not present in the older RR_API snapshot. + # Not present in the older RR_API snapshot: 49 seen on a P30 Pro; 48 and 50 from the newest V1 app plugins. + observed_docks = {48: "f1rc_dock", 49: "type_49_dock", 50: "f2rs_dock"} assert mapped_docks == RR_API_DOCK_TYPES | observed_docks @@ -392,6 +395,87 @@ def test_dock_feature_flags_with_am_variants_from_rr_api() -> None: assert shell_4rc_am.is_double_serial_communication_supported +def test_dock_type_app_name_aliases() -> None: + """App dock names for codes that already had a placeholder member resolve to the same member.""" + assert RoborockDockTypeCode.shell_4_dock is RoborockDockTypeCode.type_27_dock + assert RoborockDockTypeCode.f1rs_dock is RoborockDockTypeCode.type_49_dock + assert RoborockDockTypeCode(27).name == "type_27_dock" + assert RoborockDockTypeCode(49).name == "type_49_dock" + assert RoborockDockTypeCode["f1rs_dock"] is RoborockDockTypeCode.type_49_dock + assert "shell_4_dock" not in RoborockDockTypeCode.keys() + assert "f1rs_dock" not in RoborockDockTypeCode.keys() + + +# (dock type, has_am, capability, expected) from the DockConfigs table of the newest V1 app plugins. +@pytest.mark.parametrize( + ("dock_type", "has_am", "capability", "expected"), + [ + # F1RC (48): carousel self-clean only without AM, no water drain in either variant. + (RoborockDockTypeCode.f1rc_dock, False, "is_collect_wash_dry", True), + (RoborockDockTypeCode.f1rc_dock, False, "is_clean_carousel_self_clean_supported", True), + (RoborockDockTypeCode.f1rc_dock, True, "is_clean_carousel_self_clean_supported", False), + (RoborockDockTypeCode.f1rc_dock, False, "is_double_serial_communication_supported", True), + (RoborockDockTypeCode.f1rc_dock, False, "is_high_tmp_water_supported", True), + (RoborockDockTypeCode.f1rc_dock, False, "is_simple_wash_mode_supported", True), + (RoborockDockTypeCode.f1rc_dock, False, "is_no_water_flow_carousel", False), + (RoborockDockTypeCode.f1rc_dock, True, "is_water_updown_drain_supported", False), + # F1RS (49) and F2RS (50): no-water-flow carousel, water drain only with AM. + (RoborockDockTypeCode.f1rs_dock, False, "is_collect_wash_dry", True), + (RoborockDockTypeCode.f1rs_dock, False, "is_clean_carousel_self_clean_supported", False), + (RoborockDockTypeCode.f1rs_dock, False, "is_double_serial_communication_supported", True), + (RoborockDockTypeCode.f1rs_dock, False, "is_high_tmp_water_supported", True), + (RoborockDockTypeCode.f1rs_dock, False, "is_simple_wash_mode_supported", True), + (RoborockDockTypeCode.f1rs_dock, False, "is_no_water_flow_carousel", True), + (RoborockDockTypeCode.f1rs_dock, False, "is_water_updown_drain_supported", False), + (RoborockDockTypeCode.f1rs_dock, True, "is_water_updown_drain_supported", True), + (RoborockDockTypeCode.f2rs_dock, False, "is_collect_wash_dry", True), + (RoborockDockTypeCode.f2rs_dock, False, "is_clean_carousel_self_clean_supported", False), + (RoborockDockTypeCode.f2rs_dock, False, "is_double_serial_communication_supported", True), + (RoborockDockTypeCode.f2rs_dock, False, "is_high_tmp_water_supported", True), + (RoborockDockTypeCode.f2rs_dock, False, "is_simple_wash_mode_supported", True), + (RoborockDockTypeCode.f2rs_dock, False, "is_no_water_flow_carousel", True), + (RoborockDockTypeCode.f2rs_dock, False, "is_water_updown_drain_supported", False), + (RoborockDockTypeCode.f2rs_dock, True, "is_water_updown_drain_supported", True), + # The AM variants keep the code-only capabilities. + (RoborockDockTypeCode.f1rc_dock, True, "is_high_tmp_water_supported", True), + (RoborockDockTypeCode.f1rs_dock, True, "is_double_serial_communication_supported", True), + (RoborockDockTypeCode.f2rs_dock, True, "is_simple_wash_mode_supported", True), + # F2R (43): the newest app no longer offers carousel self-cleaning. + (RoborockDockTypeCode.f2r_dock, False, "is_clean_carousel_self_clean_supported", False), + (RoborockDockTypeCode.f1r_dock, False, "is_clean_carousel_self_clean_supported", False), + (RoborockDockTypeCode.f1_dock, False, "is_clean_carousel_self_clean_supported", True), + # K1S (20) gets automatic cleaning fluid delivery in both variants. + (RoborockDockTypeCode.k1s_dock, False, "is_clean_fluid_auto_delivery_supported", True), + (RoborockDockTypeCode.k1s_dock, True, "is_clean_fluid_auto_delivery_supported", True), + (RoborockDockTypeCode.k1_dock, False, "is_clean_fluid_auto_delivery_supported", False), + # Hera (24) has the hatch-door cool fan, like Couple. + (RoborockDockTypeCode.hera_dock, True, "is_hatch_door_dock_cool_fan_supported", True), + (RoborockDockTypeCode.couple_dock, True, "is_hatch_door_dock_cool_fan_supported", True), + (RoborockDockTypeCode.shell_3_dock, True, "is_hatch_door_dock_cool_fan_supported", False), + # Shell E (21) cannot wash mops with hot water, with or without AM. + (RoborockDockTypeCode.shell_e_dock, False, "is_no_support_hot_wash_towel", True), + (RoborockDockTypeCode.shell_e_dock, True, "is_no_support_hot_wash_towel", True), + (RoborockDockTypeCode.shell_2e_dock, False, "is_no_support_hot_wash_towel", False), + # Multiple cleaning fluids: Shell 4S (34) without AM only. + (RoborockDockTypeCode.shell_4s_dock, False, "is_multi_clean_fluid_supported", True), + (RoborockDockTypeCode.shell_4s_dock, True, "is_multi_clean_fluid_supported", False), + (RoborockDockTypeCode.shell_4p_dock, False, "is_multi_clean_fluid_supported", False), + # Carousel (strainer) present. + (RoborockDockTypeCode.pearl_dock, False, "is_clean_carousel_supported", True), + (RoborockDockTypeCode.shell_3s_dock, False, "is_clean_carousel_supported", True), + (RoborockDockTypeCode.f1rc_dock, True, "is_clean_carousel_supported", True), + (RoborockDockTypeCode.o4_dock, False, "is_clean_carousel_supported", False), + (RoborockDockTypeCode.o7_dock, True, "is_clean_carousel_supported", False), + (RoborockDockTypeCode.shell_4r_dock, False, "is_clean_carousel_supported", False), + (RoborockDockTypeCode.o0_dock, False, "is_clean_carousel_supported", False), + ], +) +def test_dock_capabilities_from_app_dock_configs( + dock_type: RoborockDockTypeCode, has_am: bool, capability: str, expected: bool +) -> None: + assert getattr(RoborockDockFeatures.from_dock_type(dock_type, has_am=has_am), capability) is expected + + @pytest.mark.parametrize( ("device_filename"), list(mock_data.DEVICE_PRODUCT_PAIRS.keys()),