diff --git a/SUPPORTED_FEATURES.md b/SUPPORTED_FEATURES.md index 752b73cf..8af3a92f 100644 --- a/SUPPORTED_FEATURES.md +++ b/SUPPORTED_FEATURES.md @@ -1,6 +1,6 @@ | Feature | roborock.vacuum.a10 | roborock.vacuum.a117 | roborock.vacuum.a123 | roborock.vacuum.a125 | roborock.vacuum.a144 | roborock.vacuum.a147 | roborock.vacuum.a15 | roborock.vacuum.a187 | roborock.vacuum.a27 | roborock.vacuum.a87 | roborock.vacuum.s5e | roborock.vacuum.sc01 | roborock.vacuum.sc05 | roborock.vacuum.ss07 | roborock.wm.a102 | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| -| product_nickname | TANOSV | PEARLPLUSS | PEARLSLITE | ULTRONSE | CORALPRO | VERDELITE | TANOSS | Unknown | TOPAZSV | PEARLPLUS | RUBYSLITE | Unknown | PEARLPLUS | Unknown | Unknown | +| product_nickname | TANOSV | PEARLPLUSS | PEARLSLITE | ULTRONSE | CORALPRO | VERDELITE | TANOSS | VIVIANC | TOPAZSV | PEARLPLUS | RUBYSLITE | Unknown | PEARLPLUS | Unknown | Unknown | | protocol_version | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | B01 | B01 | B01 | A01 | | new_feature_info | X | X | 0 | X | X | X | X | X | False | X | X | 0 | | 0 | 0 | | new_feature_info_str | | X | X | X | X | X | X | X | | X | X | X | | X | X | diff --git a/device_info.yaml b/device_info.yaml index 07bbc036..01162e1d 100644 --- a/device_info.yaml +++ b/device_info.yaml @@ -665,7 +665,7 @@ roborock.vacuum.a15: type: RAW roborock.vacuum.a187: protocol_version: '1.0' - product_nickname: Unknown + product_nickname: VIVIANC new_feature_info: 2247397454282751 new_feature_info_str: 00040040282834C9C2FA8F5C7EDEFFFE product: @@ -1303,7 +1303,7 @@ roborock.vacuum.sc05: property: 'null' roborock.vacuum.a288: protocol_version: '1.0' - product_nickname: PEARLPLUS + product_nickname: CORALPROSAW new_feature_info: 4499197267967999 new_feature_info_str: 0000000000099518CCFF7EFDA8E93EDDDBFF8F7F7EFEFFFF feature_info: diff --git a/docs/V1_DEVICE_FEATURES.md b/docs/V1_DEVICE_FEATURES.md index f9b0b9e1..70c6a31a 100644 --- a/docs/V1_DEVICE_FEATURES.md +++ b/docs/V1_DEVICE_FEATURES.md @@ -1373,6 +1373,7 @@ These features are determined by hardware capabilities and product variant. ### is_support_water_mode **Product Features:** MOP_ELECTRONIC_MODULE, MOP_SHAKE_MODULE, MOP_SPIN_MODULE +**Also enabled by:** `is_roller_mop_supported` (roller-mop models have no mop module product feature) **Feature:** Water Mode Control **Description:** Electronic control over water flow for mopping. **Impact:** Adjustable water flow rates for different floor wetness. @@ -1413,6 +1414,7 @@ These features are determined by hardware capabilities and product variant. ### is_clean_route_setting_supported **Product Features:** MOP_SHAKE_MODULE, MOP_SPIN_MODULE +**Also enabled by:** `is_shake_mop_set_supported`, `is_roller_mop_supported` **Feature:** Clean Route Settings **Description:** Configure cleaning route patterns. **Impact:** Choose between different cleaning patterns (zigzag, edge-first, etc.). @@ -1429,6 +1431,7 @@ These features are determined by hardware capabilities and product variant. ### is_customized_clean_supported **Product Features:** MOP_SHAKE_MODULE, MOP_SPIN_MODULE +**Also enabled by:** `is_roller_mop_supported` **Feature:** Customized Clean Settings **Description:** Advanced customization for mop-equipped models. **Impact:** Detailed control over mopping parameters. diff --git a/roborock/data/code_mappings.py b/roborock/data/code_mappings.py index 217cac23..25a2d615 100644 --- a/roborock/data/code_mappings.py +++ b/roborock/data/code_mappings.py @@ -179,26 +179,60 @@ def __hash__(self) -> int: class RoborockProductNickname(Enum): + # Beryl Series + BERYL = ProductInfo(nickname="Beryl", short_models=("a120", "a121")) + BERYLC = ProductInfo(nickname="BerylC", short_models=("a327", "a328")) + BERYLCE = ProductInfo(nickname="BerylCE", short_models=("a388", "a389")) + BERYLE = ProductInfo(nickname="BerylE", short_models=("a311", "a312")) + BERYLLITE = ProductInfo(nickname="BerylLite", short_models=("a244", "a245")) + BERYLS = ProductInfo(nickname="BerylS", short_models=("a306", "a307")) + # Coral Series CORAL = ProductInfo(nickname="Coral", short_models=("a20", "a21")) - CORALPRO = ProductInfo(nickname="CoralPro", short_models=("a143", "a144")) + CORALPRO = ProductInfo(nickname="CoralPro", short_models=("a143", "a144", "a231", "a232", "a236", "a308")) + CORALPROS = ProductInfo(nickname="CoralProS", short_models=("a219", "a220")) + CORALPROSAW = ProductInfo(nickname="CoralProSAw", short_models=("a287", "a288")) + + # Crag Series + CRAG = ProductInfo(nickname="Crag", short_models=("a390", "a391")) + + # Hera Series + HERA = ProductInfo(nickname="Hera", short_models=("a224", "a249")) + + # Pandora Series + PANDORA = ProductInfo(nickname="Pandora", short_models=("a243", "a250")) # Pearl Series PEARL = ProductInfo(nickname="Pearl", short_models=("a74", "a75")) PEARLC = ProductInfo(nickname="PearlC", short_models=("a103", "a104")) + PEARLCLITE = ProductInfo(nickname="PearlCLite", short_models=("a198", "a199", "a205")) PEARLE = ProductInfo(nickname="PearlE", short_models=("a167", "a168")) + PEARLEH = ProductInfo(nickname="PearlEH", short_models=("a325", "a326")) + # Newer app versions call this product PearlEPro. PEARLELITE = ProductInfo(nickname="PearlELite", short_models=("a169", "a170")) + PEARLFPRO = ProductInfo(nickname="PearlFPro", short_models=("a216", "a217")) PEARLPLUS = ProductInfo(nickname="PearlPlus", short_models=("a86", "a87")) PEARLPLUSS = ProductInfo(nickname="PearlPlusS", short_models=("a116", "a117", "a136")) PEARLS = ProductInfo(nickname="PearlS", short_models=("a100", "a101")) PEARLSLITE = ProductInfo(nickname="PearlSLite", short_models=("a122", "a123")) + # Quinn Series + QUINN = ProductInfo(nickname="Quinn", short_models=("a293", "a294")) + QUINNE = ProductInfo(nickname="QuinnE", short_models=("a358", "a359")) + QUINNS = ProductInfo(nickname="QuinnS", short_models=("a404", "a405")) + + # R Series + R50 = ProductInfo(nickname="R50", short_models=("a157", "a179")) + # Ruby Series RUBYPLUS = ProductInfo(nickname="RubyPlus", short_models=("t4", "s4")) RUBYSC = ProductInfo(nickname="RubySC", short_models=("p5", "a08")) RUBYSE = ProductInfo(nickname="RubySE", short_models=("a19",)) RUBYSLITE = ProductInfo(nickname="RubySLite", short_models=("p6", "s5e", "a05")) + # Talc Series + TALCSC = ProductInfo(nickname="TalcSC", short_models=("a360", "a361")) + # Tanos Series TANOS = ProductInfo(nickname="Tanos", short_models=("t6", "s6")) TANOSE = ProductInfo(nickname="TanosE", short_models=("t7", "a11")) @@ -228,10 +262,21 @@ class RoborockProductNickname(Enum): # Verdelite Series VERDELITE = ProductInfo(nickname="Verdelite", short_models=("a146", "a147")) + VERDELITES = ProductInfo(nickname="VerdeliteS", short_models=("a278", "a279")) + + # Vida Series + VIDA = ProductInfo(nickname="Vida", short_models=("a295", "a296")) + VIDAS = ProductInfo(nickname="VidaS", short_models=("a373", "a376")) # Vivian Series VIVIAN = ProductInfo(nickname="Vivian", short_models=("a134", "a135", "a155", "a156")) - VIVIANC = ProductInfo(nickname="VivianC", short_models=("a158", "a159")) + VIVIANC = ProductInfo(nickname="VivianC", short_models=("a158", "a159", "a186", "a187", "a209", "a210", "a246")) + VIVIANF = ProductInfo(nickname="VivianF", short_models=("a182",)) + VIVIANS = ProductInfo(nickname="VivianS", short_models=("a184", "a185")) + VIVIANSC = ProductInfo(nickname="VivianSC", short_models=("a196", "a197")) + VIVIANSPRO = ProductInfo(nickname="VivianSPro", short_models=("a225", "a226")) + VIVIANSPROC = ProductInfo(nickname="VivianSProC", short_models=("a297", "a298")) + VIVIANSPROE = ProductInfo(nickname="VivianSProE", short_models=("a374", "a375")) SHORT_MODEL_TO_ENUM = {model: product for product in RoborockProductNickname for model in product.value.short_models} diff --git a/roborock/device_features.py b/roborock/device_features.py index 160d14cc..94ffc6f7 100644 --- a/roborock/device_features.py +++ b/roborock/device_features.py @@ -153,6 +153,8 @@ class ProductFeatures(StrEnum): ULTRONSV_FEATURES = NEW_DEFAULT_FEATURES + RGB_CAMERA_FEATURES + [ProductFeatures.MOP_SHAKE_MODULE] TANOSS_FEATURES = [ProductFeatures.REMOTE_BACK, ProductFeatures.MOP_SHAKE_MODULE] TOPAZSPOWER_FEATURES = [ProductFeatures.CLEANMODE_MAXPLUS, ProductFeatures.MOP_SHAKE_MODULE] +BERYL_FEATURES = [ProductFeatures.CLEANMODE_MAXPLUS] + RGB_CAMERA_FEATURES +VIVIANSPROC_FEATURES = BERYL_FEATURES + [ProductFeatures.MOP_SPIN_MODULE] PRODUCTS_WITHOUT_CUSTOM_CLEAN: set[RoborockProductNickname] = { RoborockProductNickname.TANOS, @@ -234,6 +236,35 @@ class ProductFeatures(StrEnum): RoborockProductNickname.RUBYSC: [], RoborockProductNickname.RUBYSE: [], RoborockProductNickname.RUBYSLITE: [ProductFeatures.MOP_ELECTRONIC_MODULE], + RoborockProductNickname.PEARLCLITE: PEARL_FEATURES, + RoborockProductNickname.PEARLEH: PEARL_FEATURES, + RoborockProductNickname.QUINN: PEARL_FEATURES, + RoborockProductNickname.QUINNE: PEARL_FEATURES, + RoborockProductNickname.QUINNS: PEARL_FEATURES, + RoborockProductNickname.VIVIANSPROE: PEARL_FEATURES, + RoborockProductNickname.VIVIANF: PEARL_PLUS_FEATURES, + RoborockProductNickname.VIVIANS: PEARL_PLUS_FEATURES, + RoborockProductNickname.VIVIANSC: PEARL_PLUS_FEATURES, + RoborockProductNickname.VIVIANSPRO: PEARL_PLUS_FEATURES, + RoborockProductNickname.HERA: PEARL_PLUS_FEATURES, + RoborockProductNickname.PANDORA: PEARL_PLUS_FEATURES, + RoborockProductNickname.PEARLFPRO: PEARL_PLUS_FEATURES, + RoborockProductNickname.CORALPROS: PEARL_PLUS_FEATURES, + RoborockProductNickname.R50: PEARL_PLUS_FEATURES, + RoborockProductNickname.VIVIANSPROC: VIVIANSPROC_FEATURES, + RoborockProductNickname.VIDA: VIVIANSPROC_FEATURES, + RoborockProductNickname.VIDAS: VIVIANSPROC_FEATURES, + RoborockProductNickname.CORALPROSAW: VIVIANSPROC_FEATURES, + RoborockProductNickname.BERYLLITE: BERYL_FEATURES, + RoborockProductNickname.BERYL: BERYL_FEATURES, + RoborockProductNickname.BERYLS: BERYL_FEATURES, + RoborockProductNickname.BERYLE: BERYL_FEATURES, + RoborockProductNickname.BERYLC: BERYL_FEATURES, + RoborockProductNickname.CRAG: BERYL_FEATURES, + RoborockProductNickname.BERYLCE: [ProductFeatures.CLEANMODE_MAXPLUS, ProductFeatures.AIRECOGNITION_PET] + + SINGLE_LINE_CAMERA_FEATURES, + RoborockProductNickname.VERDELITES: BERYL_FEATURES + [ProductFeatures.MOP_SHAKE_MODULE], + RoborockProductNickname.TALCSC: [ProductFeatures.MOP_ELECTRONIC_MODULE], } PRODUCT_FEATURE_MAP: dict[RoborockProductNickname, list[ProductFeatures]] = { @@ -651,6 +682,10 @@ def from_feature_flags( kwargs["is_clean_route_setting_supported"] |= ( kwargs["is_shake_mop_set_supported"] or kwargs["is_roller_mop_supported"] ) + # Roller-mop models have no mop module product feature; the app treats the + # roller mop like one for water modes and customized cleaning. + kwargs["is_support_water_mode"] |= kwargs["is_roller_mop_supported"] + kwargs["is_customized_clean_supported"] |= kwargs["is_roller_mop_supported"] return cls(**kwargs) diff --git a/tests/__snapshots__/test_supported_features.ambr b/tests/__snapshots__/test_supported_features.ambr index dfd9f8ac..b5be6a35 100644 --- a/tests/__snapshots__/test_supported_features.ambr +++ b/tests/__snapshots__/test_supported_features.ambr @@ -90,7 +90,7 @@ 'is_new_data_for_clean_history': True, 'is_new_data_for_clean_history_detail': True, 'is_new_endpoint_supported': True, - 'is_new_remote_view_supported': True, + 'is_new_remote_view_supported': False, 'is_no_need_carpet_press_set_supported': True, 'is_none_pure_clean_mop_with_max_plus': False, 'is_object_detect_check_supported': True, @@ -1968,3 +1968,876 @@ 'is_workday_holiday_supported': False, }) # --- +# name: test_product_feature_map[BERYLCE] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[BERYLC] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[BERYLE] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[BERYLLITE] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[BERYLS] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[BERYL] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[CORALPROSAW] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[CORALPROS] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[CORALPRO] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[CORAL] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[CRAG] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[HERA] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PANDORA] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARLCLITE] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARLC] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARLEH] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARLELITE] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARLE] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARLFPRO] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARLPLUSS] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARLPLUS] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARLSLITE] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARLS] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[PEARL] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[QUINNE] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[QUINNS] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[QUINN] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[R50] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[RUBYPLUS] + list([ + ]) +# --- +# name: test_product_feature_map[RUBYSC] + list([ + ]) +# --- +# name: test_product_feature_map[RUBYSE] + list([ + ]) +# --- +# name: test_product_feature_map[RUBYSLITE] + list([ + , + , + , + ]) +# --- +# name: test_product_feature_map[TALCSC] + list([ + , + , + , + , + ]) +# --- +# name: test_product_feature_map[TANOSE] + list([ + , + , + , + ]) +# --- +# name: test_product_feature_map[TANOSSC] + list([ + , + , + ]) +# --- +# name: test_product_feature_map[TANOSSE] + list([ + , + , + ]) +# --- +# name: test_product_feature_map[TANOSSLITE] + list([ + , + , + , + ]) +# --- +# name: test_product_feature_map[TANOSSMAX] + list([ + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[TANOSSPLUS] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[TANOSS] + list([ + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[TANOSV] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[TANOS] + list([ + , + ]) +# --- +# name: test_product_feature_map[TOPAZSC] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[TOPAZSPLUS] + list([ + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[TOPAZSPOWER] + list([ + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[TOPAZSV] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[TOPAZS] + list([ + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[ULTRONE] + list([ + , + , + , + ]) +# --- +# name: test_product_feature_map[ULTRONLITE] + list([ + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[ULTRONSC] + list([ + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[ULTRONSE] + list([ + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[ULTRONSPLUS] + list([ + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[ULTRONSV] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[ULTRON] + list([ + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VERDELITES] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VERDELITE] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VIDAS] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VIDA] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VIVIANC] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VIVIANF] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VIVIANSC] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VIVIANSPROC] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VIVIANSPROE] + list([ + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VIVIANSPRO] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VIVIANS] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- +# name: test_product_feature_map[VIVIAN] + list([ + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ]) +# --- diff --git a/tests/data/test_code_mappings.py b/tests/data/test_code_mappings.py index 741046fb..1e46c750 100644 --- a/tests/data/test_code_mappings.py +++ b/tests/data/test_code_mappings.py @@ -8,7 +8,7 @@ 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.code_mappings import SHORT_MODEL_TO_ENUM, RoborockProductNickname, completed_warnings from roborock.data.dyad.dyad_code_mappings import DyadCleanMode, DyadError from roborock.data.v1.v1_code_mappings import ( RoborockErrorCode, @@ -108,6 +108,42 @@ def test_from_any_optional(input_value: str | int, expected: B01_Q10_DP | None) assert B01_Q10_DP.from_any_optional(input_value) == expected +@pytest.mark.parametrize( + ("short_model", "expected"), + [ + ("a144", RoborockProductNickname.CORALPRO), + ("a308", RoborockProductNickname.CORALPRO), + ("a288", RoborockProductNickname.CORALPROSAW), + ("a187", RoborockProductNickname.VIVIANC), + ("a210", RoborockProductNickname.VIVIANC), + ("a185", RoborockProductNickname.VIVIANS), + ("a226", RoborockProductNickname.VIVIANSPRO), + ("a298", RoborockProductNickname.VIVIANSPROC), + ("a296", RoborockProductNickname.VIDA), + ("a245", RoborockProductNickname.BERYLLITE), + ("a306", RoborockProductNickname.BERYLS), + ("a328", RoborockProductNickname.BERYLC), + ("a199", RoborockProductNickname.PEARLCLITE), + ("a326", RoborockProductNickname.PEARLEH), + ("a170", RoborockProductNickname.PEARLELITE), + ("a294", RoborockProductNickname.QUINN), + ("a179", RoborockProductNickname.R50), + ("a279", RoborockProductNickname.VERDELITES), + ("a361", RoborockProductNickname.TALCSC), + ("a999", None), + ], +) +def test_short_model_to_product_nickname(short_model: str, expected: RoborockProductNickname | None) -> None: + """Test short models resolve to the product nickname the app assigns them.""" + assert SHORT_MODEL_TO_ENUM.get(short_model) == expected + + +def test_short_models_belong_to_one_product_nickname() -> None: + """Test no short model is listed under two product nicknames.""" + short_models = [model for product in RoborockProductNickname for model in product.value.short_models] + assert len(short_models) == len(set(short_models)) + + def test_homedata_product_unknown_category(): """Test that HomeDataProduct can handle unknown categories.""" data = { diff --git a/tests/data/test_containers.py b/tests/data/test_containers.py index 27ca1e3e..1cc06d2c 100644 --- a/tests/data/test_containers.py +++ b/tests/data/test_containers.py @@ -6,9 +6,11 @@ import pytest +from roborock.data.code_mappings import RoborockProductNickname from roborock.data.containers import ( HomeData, HomeDataDevice, + HomeDataProduct, RoborockBase, RoborockCategory, UserData, @@ -267,6 +269,22 @@ def test_home_data(): assert hd.rooms[0].name == "Example room 1" +@pytest.mark.parametrize( + ("model", "expected"), + [ + ("roborock.vacuum.a144", RoborockProductNickname.CORALPRO), + ("roborock.vacuum.a307", RoborockProductNickname.BERYLS), + ("roborock.vacuum.a187", RoborockProductNickname.VIVIANC), + ("roborock.vacuum.a999", RoborockProductNickname.PEARLPLUS), + ], +) +def test_home_data_product_nickname(model: str, expected: RoborockProductNickname) -> None: + """Test the product nickname comes from the short model and falls back to PearlPlus.""" + product = HomeDataProduct(id="product-id", name="Vacuum", model=model, category=RoborockCategory.VACUUM) + + assert product.product_nickname == expected + + def test_serialize_and_unserialize(): ud = UserData.from_dict(USER_DATA) ud_dict = ud.as_dict() diff --git a/tests/test_supported_features.py b/tests/test_supported_features.py index b427ccc5..1cd7df2b 100644 --- a/tests/test_supported_features.py +++ b/tests/test_supported_features.py @@ -9,7 +9,13 @@ from roborock.data import HomeDataProduct from roborock.data.code_mappings import RoborockProductNickname from roborock.data.v1 import RoborockDockTypeCode -from roborock.device_features import DeviceFeatures, RoborockDockFeatures, is_valid_dock, is_wash_n_fill_dock +from roborock.device_features import ( + PRODUCT_FEATURE_MAP, + DeviceFeatures, + RoborockDockFeatures, + is_valid_dock, + is_wash_n_fill_dock, +) from roborock.devices.cache import CacheData, DeviceCache, InMemoryCache from roborock.devices.traits.v1.device_features import DeviceFeaturesTrait from roborock.roborock_typing import RoborockCommand @@ -137,6 +143,37 @@ def test_new_feature_str_missing(): assert not device_features.is_show_clean_finish_reason_supported +@pytest.mark.parametrize("product_nickname", list(RoborockProductNickname), ids=lambda product: product.name) +def test_product_feature_map(product_nickname: RoborockProductNickname, snapshot: SnapshotAssertion) -> None: + """Snapshot the product features of every nickname so changes show up per product.""" + assert PRODUCT_FEATURE_MAP[product_nickname] == snapshot + + +@pytest.mark.parametrize( + ("roller_mop_bit", "expected_mop_support"), + [ + (0, False), + (1 << 128, True), + ], +) +def test_roller_mop_product_features(roller_mop_bit: int, expected_mop_support: bool) -> None: + """Test a roller-mop model without a mop module gets its mop support from the roller-mop bit.""" + device_features = DeviceFeatures.from_feature_flags( + new_feature_info=0, + new_feature_info_str=f"{roller_mop_bit:X}", + feature_info=[], + product_nickname=RoborockProductNickname.BERYLS, + ) + + assert device_features.is_max_plus_mode_supported + assert device_features.is_ai_recognition_setting_supported + assert device_features.is_pure_clean_mop_supported + assert not device_features.is_new_remote_view_supported + assert device_features.is_support_water_mode is expected_mop_support + assert device_features.is_customized_clean_supported is expected_mop_support + assert device_features.is_clean_route_setting_supported is expected_mop_support + + def test_clean_route_setting_support_from_shake_mop_flag() -> None: """Test clean-route capability includes the runtime shake-mop flag.""" device_features = DeviceFeatures.from_feature_flags( @@ -172,8 +209,8 @@ def test_clean_route_setting_support_from_spin_mop_model() -> None: (129, False), ], ) -def test_clean_route_setting_support_from_roller_mop_bit(feature_bit: int, expected: bool) -> None: - """Test only the RR_API roller-mop bit adds roller route support.""" +def test_mop_support_from_roller_mop_bit(feature_bit: int, expected: bool) -> None: + """Test only the RR_API roller-mop bit adds mop support to models without a mop module.""" device_features = DeviceFeatures.from_feature_flags( new_feature_info=0, new_feature_info_str=f"{1 << feature_bit:X}", @@ -183,6 +220,8 @@ def test_clean_route_setting_support_from_roller_mop_bit(feature_bit: int, expec assert device_features.is_roller_mop_supported is expected assert device_features.is_clean_route_setting_supported is expected + assert device_features.is_support_water_mode is expected + assert device_features.is_customized_clean_supported is expected @pytest.mark.parametrize(