Skip to content
Open
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
56 changes: 56 additions & 0 deletions docs/V1_DEVICE_FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down Expand Up @@ -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.
10 changes: 10 additions & 0 deletions roborock/data/v1/v1_code_mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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):
Expand Down
83 changes: 81 additions & 2 deletions roborock/device_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,14 +719,14 @@ 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 = {
RoborockDockTypeCode.shell_2s_dock,
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,
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -852,19 +861,69 @@ 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 = {
RoborockDockTypeCode.f1_dock,
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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
86 changes: 85 additions & 1 deletion tests/test_supported_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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


Expand Down Expand Up @@ -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()),
Expand Down
Loading