diff --git a/device_info.yaml b/device_info.yaml index 07bbc036..4bc4b97f 100644 --- a/device_info.yaml +++ b/device_info.yaml @@ -877,6 +877,158 @@ roborock.vacuum.a27: code: offline_status mode: ro type: RAW +roborock.vacuum.a294: + protocol_version: '1.0' + product_nickname: Unknown + new_feature_info: 2247397454282751 + new_feature_info_str: 0000009021D0B410C0C47EFDA82934C952BA8F507EDEFFFE + feature_info: + - 111 + - 112 + - 113 + - 114 + - 115 + - 116 + - 117 + - 118 + - 119 + - 120 + - 121 + - 122 + - 123 + - 124 + - 125 + product: + id: aZagb9tLAoAq2UbmGnFx0 + name: Roborock Qrevo 2 Pro Series + model: roborock.vacuum.a294 + category: robot.vacuum.cleaner + capability: 0 + schema: + - id: 101 + name: rpc_request + code: rpc_request + mode: rw + type: RAW + - id: 102 + name: rpc_response + code: rpc_response + mode: rw + type: RAW + - id: 120 + name: "\u9519\u8BEF\u4EE3\u7801" + code: error_code + mode: ro + type: ENUM + property: '{"range": [""]}' + - id: 121 + name: "\u8BBE\u5907\u72B6\u6001" + code: state + mode: ro + type: ENUM + property: '{"range": [""]}' + - id: 122 + name: "\u8BBE\u5907\u7535\u91CF" + code: battery + mode: ro + type: ENUM + property: '{"range": [""]}' + - id: 123 + name: "\u6E05\u626B\u6A21\u5F0F" + code: fan_power + mode: rw + type: ENUM + property: '{"range": [""]}' + - id: 124 + name: "\u62D6\u5730\u6A21\u5F0F" + code: water_box_mode + mode: rw + type: ENUM + property: '{"range": [""]}' + - id: 125 + name: "\u4E3B\u5237\u5BFF\u547D" + code: main_brush_life + mode: rw + type: VALUE + property: '{"max": 100, "min": 0, "step": 1, "unit": "null", "scale": 1}' + - id: 126 + name: "\u8FB9\u5237\u5BFF\u547D" + code: side_brush_life + mode: rw + type: VALUE + property: '{"max": 100, "min": 0, "step": 1, "unit": "null", "scale": 1}' + - id: 127 + name: "\u6EE4\u7F51\u5BFF\u547D" + code: filter_life + mode: rw + type: VALUE + property: '{"max": 100, "min": 0, "step": 1, "unit": "null", "scale": 1}' + - id: 128 + name: "\u989D\u5916\u72B6\u6001" + code: additional_props + mode: ro + type: RAW + - id: 130 + name: "\u5B8C\u6210\u4E8B\u4EF6" + code: task_complete + mode: ro + type: RAW + - id: 131 + name: "\u7535\u91CF\u4E0D\u8DB3\u4EFB\u52A1\u53D6\u6D88" + code: task_cancel_low_power + mode: ro + type: RAW + - id: 132 + name: "\u8FD0\u52A8\u4E2D\u4EFB\u52A1\u53D6\u6D88" + code: task_cancel_in_motion + mode: ro + type: RAW + - id: 133 + name: "\u5145\u7535\u72B6\u6001" + code: charge_status + mode: ro + type: RAW + - id: 134 + name: "\u70D8\u5E72\u72B6\u6001" + code: drying_status + mode: ro + type: RAW + - id: 135 + name: "\u79BB\u7EBF\u539F\u56E0\u7EC6\u5206" + code: offline_status + mode: ro + type: RAW + - id: 138 + name: "\u5DE5\u4F5C\u4EFB\u52A1\u7C7B\u578B" + code: clean_task_type + mode: ro + type: ENUM + property: "{\"range\": [\"0 \u7A7A\u95F2\", \"1 \u5168\u5C4B\u6E05\u6D01\",\ + \ \"2 \u9009\u533A\u6E05\u6D01\", \"3 \u5212\u533A\u6E05\u6D01\", \"4 \u5EFA\ + \u56FE\", \"5 \u5C40\u90E8\u6E05\u6D01\", \"6 \u9065\u63A7\u6A21\u5F0F\",\ + \ \"7 \u5DE1\u822A\", \"8 \u5BFB\u5BA0\", \"9 \u6574\u7406\"]}" + - id: 139 + name: "\u56DE\u57FA\u7AD9\u76EE\u7684" + code: back_type + mode: ro + type: RAW + - id: 141 + name: "\u6E05\u6D01\u8FDB\u5EA6" + code: cleaning_progress + mode: ro + type: VALUE + property: "{\"max\": 100, \"min\": 0, \"step\": 1, \"unit\": \"\u767E\u5206\u6BD4\ + \", \"scale\": 1}" + - id: 142 + name: publish_dsp + code: publish_dsp + mode: ro + type: RAW + - id: 143 + name: "\u5730\u56FE\u53D8\u5316\u6570\u636E" + code: map_diff + mode: ro + type: RAW roborock.vacuum.a75: protocol_version: '1.0' product_nickname: PEARL diff --git a/roborock/devices/traits/v1/device_features.py b/roborock/devices/traits/v1/device_features.py index 1f785e65..f8ecb135 100644 --- a/roborock/devices/traits/v1/device_features.py +++ b/roborock/devices/traits/v1/device_features.py @@ -9,6 +9,9 @@ from roborock.devices.traits.v1 import common from roborock.roborock_typing import RoborockCommand +# Qrevo 2 Pro uses single-line laser obstacle sensing without an RGB camera. +_MODELS_WITHOUT_OBSTACLE_PHOTOS = frozenset({"roborock.vacuum.a294"}) + # Cache of metadata for each trait class _metadata_cache: dict[type[RoborockBase], dict[str, dict[str, Any]]] = {} @@ -69,6 +72,11 @@ def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None: for field in fields(self): setattr(self, field.name, False) + @property + def is_obstacle_photo_download_supported(self) -> bool: + """Return whether obstacle-photo downloads are supported for trait discovery.""" + return self.is_ai_recognition_obstacle_supported and self._product.model not in _MODELS_WITHOUT_OBSTACLE_PHOTOS + def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool: """Determines if the specified field is supported by this device. diff --git a/roborock/devices/traits/v1/obstacle_photos.py b/roborock/devices/traits/v1/obstacle_photos.py index 3a8d9882..11de2794 100644 --- a/roborock/devices/traits/v1/obstacle_photos.py +++ b/roborock/devices/traits/v1/obstacle_photos.py @@ -74,7 +74,7 @@ class ObstaclePhotoTrait(RoborockBase, common.V1TraitMixin): command = RoborockCommand.GET_PHOTO converter = ObstaclePhotoConverter() blob_rpc_channel = True - requires_feature = "is_ai_recognition_obstacle_supported" + requires_feature = "is_obstacle_photo_download_supported" def __init__(self, standard_rpc_channel: V1RpcChannel) -> None: """Initialize the obstacle photo trait.""" diff --git a/tests/__snapshots__/test_supported_features.ambr b/tests/__snapshots__/test_supported_features.ambr index dfd9f8ac..834cc350 100644 --- a/tests/__snapshots__/test_supported_features.ambr +++ b/tests/__snapshots__/test_supported_features.ambr @@ -1073,6 +1073,185 @@ 'is_workday_holiday_supported': False, }) # --- +# name: test_device_features_from_home_data[home_data_device_qrevo_2_pro.json] + dict({ + 'is_activate_video_charging_and_standby_supported': False, + 'is_ai_recognition_obstacle_supported': True, + 'is_ai_recognition_setting_supported': True, + 'is_analysis_supported': False, + 'is_any_state_transit_goto_supported': True, + 'is_auto_collection_2_supported': True, + 'is_auto_delivery_field_in_global_status_supported': False, + 'is_auto_tear_down_mop_supported': True, + 'is_avoid_collision_mode_supported': True, + 'is_avoid_collision_supported': True, + 'is_back_charge_auto_wash_supported': True, + 'is_back_wash_new_smart_supported': True, + 'is_careful_slow_mop_supported': True, + 'is_carpet_custom_clean_supported': True, + 'is_carpet_deep_clean_supported': True, + 'is_carpet_long_haired_ex_supported': False, + 'is_carpet_long_haired_supported': False, + 'is_carpet_pressure_use_origin_paras_supported': False, + 'is_carpet_shape_type_supported': True, + 'is_carpet_show_on_map': True, + 'is_carpet_supported': True, + 'is_ces_2022_supported': False, + 'is_clean_count_setting_supported': True, + 'is_clean_direct_status_supported': True, + 'is_clean_efficiency_supported': True, + 'is_clean_fluid_delivery_supported': False, + 'is_clean_history_time_line_supported': True, + 'is_clean_route_deep_slow_plus_supported': False, + 'is_clean_route_fast_mode_supported': True, + 'is_clean_route_setting_supported': True, + 'is_clean_then_mop_mode_supported': True, + 'is_clean_time_line_supported': True, + 'is_collect_dust_count_show_supported': True, + 'is_collect_dust_mode_supported': True, + 'is_corner_clean_mode_supported': False, + 'is_corner_mop_stretch_supported': True, + 'is_ctm_with_repeat_supported': True, + 'is_current_map_restore_enabled': True, + 'is_custom_clean_mode_count_supported': True, + 'is_custom_mode_supported': True, + 'is_custom_water_box_distance_supported': True, + 'is_customized_clean_supported': True, + 'is_detect_wire_carpet_supported': True, + 'is_dirty_object_detect_supported': False, + 'is_dirty_replenish_clean_supported': False, + 'is_dry_interval_timer_supported': True, + 'is_dss_believable': True, + 'is_dual_band_wi_fi_supported': True, + 'is_dust_collection_setting_supported': True, + 'is_dynamically_add_clean_zones_supported': True, + 'is_dynamically_skip_clean_zone_supported': True, + 'is_egg_dance_mode_supported': False, + 'is_egg_mode_supported_from_new_features': False, + 'is_exact_custom_mode_supported': True, + 'is_exhibition_function_supported': False, + 'is_floor_dir_clean_any_time_supported': True, + 'is_flow_led_setting_supported': False, + 'is_follow_low_obs_supported': False, + 'is_full_duples_switch_supported': False, + 'is_fw_filter_obstacle_supported': True, + 'is_gap_deep_clean_supported': False, + 'is_goto_pure_clean_path_supported': True, + 'is_hot_wash_towel_supported': True, + 'is_identify_room_supported': False, + 'is_ignore_unknown_map_object_supported': True, + 'is_lds_lifting_supported': False, + 'is_led_status_switch_supported': False, + 'is_left_water_drain_supported': True, + 'is_low_area_access_supported': False, + 'is_main_brush_up_down_supported_from_str': True, + 'is_map_beautify_internal_debug_supported': True, + 'is_map_carpet_add_support': True, + 'is_map_eraser_supported': True, + 'is_matter_supported': True, + 'is_max_plus_mode_supported': True, + 'is_max_zone_opened_supported': True, + 'is_mechanical_arm_mode_supported': False, + 'is_midway_back_to_dock_supported': True, + 'is_min_battery_15_to_clean_task_supported': True, + 'is_mop_forbidden_supported': False, + 'is_mop_path_supported': True, + 'is_mop_shake_module_supported': False, + 'is_mop_shake_water_max_supported': False, + 'is_multi_floor_supported': False, + 'is_multi_map_segment_timer_supported': True, + 'is_new_ai_recognition_supported': True, + '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_no_need_carpet_press_set_supported': True, + 'is_none_pure_clean_mop_with_max_plus': False, + 'is_object_detect_check_supported': True, + 'is_offline_map_supported': True, + 'is_optimize_battery_supported': False, + 'is_order_clean_supported': False, + 'is_over_sea_ctm_supported': True, + 'is_pet_snapshot_supported': False, + 'is_pet_supplies_deep_clean_supported': True, + 'is_program_mode_supported': False, + 'is_pumping_water_supported': False, + 'is_pure_clean_mop_supported': True, + 'is_re_segment_supported': True, + '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, + 'is_set_child_supported': True, + 'is_setting_carpet_first_supported': True, + 'is_shake_mop_set_supported': True, + 'is_should_show_arm_over_load_supported': True, + 'is_show_clean_finish_reason_supported': True, + 'is_show_general_obstacle_supported': False, + 'is_show_obstacle_photo_supported': False, + 'is_side_brush_lift_carpet_supported': False, + 'is_small_side_mop_supported': False, + 'is_smart_clean_mode_set_supported': True, + 'is_soak_and_wash_supported': True, + 'is_soft_clean_mode_supported': False, + 'is_sr_map_supported': False, + 'is_super_deep_wash_supported': True, + 'is_support_api_app_stop_grasp_supported': True, + 'is_support_backup_map': True, + 'is_support_clean_estimate': True, + 'is_support_cliff_zone': True, + 'is_support_custom_carpet': False, + 'is_support_custom_dnd': True, + 'is_support_custom_door_sill': True, + 'is_support_custom_mode_in_cleaning': True, + 'is_support_fetch_timer_summary': False, + 'is_support_floor_direction': True, + 'is_support_floor_edit': True, + 'is_support_furniture': True, + 'is_support_get_particular_status_supported': True, + 'is_support_incremental_map': True, + 'is_support_main_brush_up_down_supported': False, + 'is_support_mop_back_pwm_set': False, + 'is_support_quick_map_builder': True, + 'is_support_remote_control_in_call': False, + 'is_support_room_tag': True, + 'is_support_set_switch_map_mode': True, + 'is_support_set_volume_in_call': False, + 'is_support_side_brush_up_down_supported': False, + 'is_support_smart_door_sill': True, + 'is_support_smart_global_clean_with_custom_mode': True, + 'is_support_smart_scene': True, + 'is_support_stuck_zone': True, + 'is_support_voice_control_debug': False, + 'is_support_water_mode': True, + 'is_supported_download_test_voice': True, + 'is_supported_drying': True, + 'is_supported_valley_electricity': True, + 'is_sync_server_name_supported': True, + 'is_three_d_mapping_inner_test_supported': False, + 'is_tidyup_zones_supported': False, + 'is_two_gears_no_collision_supported': True, + 'is_two_key_real_time_video_supported': False, + 'is_two_key_rtv_in_charging_supported': False, + 'is_type_identify_supported': False, + 'is_unsave_map_reason_supported': True, + 'is_uvc_sterilize_supported': False, + 'is_video_monitor_supported': True, + 'is_video_patrol_supported': False, + 'is_video_setting_supported': True, + 'is_voice_control_led_supported': False, + 'is_voice_control_supported': False, + 'is_wash_then_charge_cmd_supported': True, + 'is_water_leak_check_supported': True, + 'is_water_slide_mode_supported': True, + 'is_water_up_down_drain_supported': True, + 'is_wifi_manage_supported': True, + 'is_workday_holiday_supported': False, + }) +# --- # name: test_device_features_from_home_data[home_data_device_s5e.json] dict({ 'is_activate_video_charging_and_standby_supported': False, diff --git a/tests/devices/traits/v1/__snapshots__/test_device_features.ambr b/tests/devices/traits/v1/__snapshots__/test_device_features.ambr index a6eddf96..59c4208f 100644 --- a/tests/devices/traits/v1/__snapshots__/test_device_features.ambr +++ b/tests/devices/traits/v1/__snapshots__/test_device_features.ambr @@ -79,6 +79,26 @@ 'water_shortage_status': True, }) # --- +# name: test_is_attribute_supported[home_data_device_qrevo_2_pro.json] + dict({ + 'battery': True, + 'charge_status': True, + 'clean_fluid_status': False, + 'clean_percent': True, + 'clear_water_box_status': True, + 'dirty_water_box_status': True, + 'dock_error_status': True, + 'dry_status': True, + 'error_code': True, + 'fan_power': True, + 'rdt': True, + 'state': True, + 'water_box_carriage_status': True, + 'water_box_mode': True, + 'water_box_status': True, + 'water_shortage_status': True, + }) +# --- # name: test_is_attribute_supported[home_data_device_s5e.json] dict({ 'battery': True, @@ -187,6 +207,13 @@ 'side_brush_work_time': True, }) # --- +# name: test_is_consumable_field_supported[home_data_device_qrevo_2_pro.json] + dict({ + 'filter_work_time': True, + 'main_brush_work_time': True, + 'side_brush_work_time': True, + }) +# --- # name: test_is_consumable_field_supported[home_data_device_s5e.json] dict({ 'filter_work_time': True, diff --git a/tests/devices/traits/v1/test_obstacle_photos.py b/tests/devices/traits/v1/test_obstacle_photos.py index 5597ec3e..ec2a807c 100644 --- a/tests/devices/traits/v1/test_obstacle_photos.py +++ b/tests/devices/traits/v1/test_obstacle_photos.py @@ -5,6 +5,7 @@ import pytest +from roborock.data import HomeDataDevice from roborock.devices.device import RoborockDevice from roborock.devices.traits.v1.obstacle_photos import ( ObstaclePhotoTrait, @@ -15,6 +16,7 @@ from roborock.protocols.v1_protocol import create_blob_response_decoder from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol from roborock.roborock_typing import RoborockCommand +from tests import mock_data PNG_BYTES = b"\x89PNG\r\n\x1a\nphoto-bytes" JPEG_BYTES = b"\xff\xd8\xff\xe0photo-bytes" @@ -114,7 +116,27 @@ def test_decode_blob_response_does_not_wrap_unexpected_errors() -> None: def test_obstacle_photo_trait_metadata() -> None: """Test obstacle photos use the blob RPC channel when discovered.""" assert ObstaclePhotoTrait.blob_rpc_channel is True - assert ObstaclePhotoTrait.requires_feature == "is_ai_recognition_obstacle_supported" + assert ObstaclePhotoTrait.requires_feature == "is_obstacle_photo_download_supported" + + +@pytest.mark.parametrize( + ("device_info", "photos_supported"), + [ + (mock_data.DEVICE_PRODUCT_PAIRS["home_data_device_qrevo_2_pro.json"][0], False), + (mock_data.DEVICE_PRODUCT_PAIRS["home_data_device_s7_maxv.json"][0], True), + ], + ids=["qrevo_2_pro", "s7_maxv"], +) +async def test_obstacle_photo_discovery( + device_info: HomeDataDevice, + photos_supported: bool, + device: RoborockDevice, + discover_features_fixture: None, +) -> None: + """Discover photo downloads separately from generic obstacle recognition.""" + assert device.v1_properties is not None + assert device.v1_properties.device_features.is_ai_recognition_obstacle_supported is True + assert (device.v1_properties.obstacle_photos is not None) is photos_supported async def test_get_enabled(obstacle_photo_trait: ObstaclePhotoTrait, mock_rpc_channel: AsyncMock) -> None: diff --git a/tests/devices/traits/v1/test_status.py b/tests/devices/traits/v1/test_status.py index b30b24ad..72274ca6 100644 --- a/tests/devices/traits/v1/test_status.py +++ b/tests/devices/traits/v1/test_status.py @@ -117,6 +117,46 @@ async def test_refresh_status_p30_pro( mock_rpc_channel.send_command.assert_awaited_once_with(RoborockCommand.GET_STATUS) +@pytest.mark.parametrize( + "device_info", + [mock_data.DEVICE_PRODUCT_PAIRS["home_data_device_qrevo_2_pro.json"][0]], +) +@pytest.mark.parametrize( + ("fan_power", "water_box_mode", "mop_mode", "expected_water_mode", "expected_mop_route"), + [ + (102, 235, 300, "medium", "standard"), + (104, 243, 303, "high", "deep_plus"), + ], +) +async def test_refresh_status_qrevo_2_pro( + device_info: HomeDataDevice, + discover_features_fixture: None, + status_trait: StatusTrait, + mock_rpc_channel: AsyncMock, + fan_power: int, + water_box_mode: int, + mop_mode: int, + expected_water_mode: str, + expected_mop_route: str, +) -> None: + """Test captured Qrevo 2 Pro water levels and mop routes after discovery.""" + mock_rpc_channel.send_command.return_value = [ + { + **STATUS, + "fan_power": fan_power, + "water_box_mode": water_box_mode, + "mop_mode": mop_mode, + "dock_type": RoborockDockTypeCode.f1_dock, + } + ] + + await status_trait.refresh() + + assert status_trait.water_mode_name == expected_water_mode + assert status_trait.mop_route_name == expected_mop_route + mock_rpc_channel.send_command.assert_awaited_once_with(RoborockCommand.GET_STATUS) + + async def test_refresh_status_propagates_exception(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None: """Test that exceptions from RPC channel are propagated.""" mock_rpc_channel.send_command.side_effect = RoborockException("Communication error") diff --git a/tests/mock_data.py b/tests/mock_data.py index 353d8b7f..d612dd0e 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -178,6 +178,7 @@ "product-saros-10": RoborockDockTypeCode.o4_dock, # Saros 10 / Q Revo Pro (Wash/Empty/Auto Fluid) "product-saros-10r": RoborockDockTypeCode.o6_dock, # JC / Q Revo Master (Wash/Empty/Hot water) "product-id-a306": RoborockDockTypeCode.type_49_dock, # P30 Pro (Wash/Empty/Dry) + "product-id-a294": RoborockDockTypeCode.f1_dock, # Qrevo 2 Pro (Wash/Empty/Dry) } diff --git a/tests/testdata/home_data_device_qrevo_2_pro.json b/tests/testdata/home_data_device_qrevo_2_pro.json new file mode 100644 index 00000000..d7db2c24 --- /dev/null +++ b/tests/testdata/home_data_device_qrevo_2_pro.json @@ -0,0 +1,30 @@ +{ + "fv": "02.40.54", + "share": false, + "online": true, + "pv": "1.0", + "tuyaMigrated": false, + "featureSet": "2247397454282751", + "newFeatureSet": "0000009021D0B410C0C47EFDA82934C952BA8F507EDEFFFE", + "deviceStatus": { + "121": 8, + "122": 64, + "133": 1, + "123": 102, + "134": 0, + "124": 202, + "128": 0, + "139": 0, + "135": 0, + "125": 100, + "126": 100, + "127": 100 + }, + "silentOtaSwitch": true, + "f": false, + "duid": "device-id-a294", + "name": "Roborock Qrevo 2 Pro", + "localKey": "0123456789abcdef", + "productId": "product-id-a294", + "sn": "A294SAMPLESNTEST" +} diff --git a/tests/testdata/home_data_product_a294.json b/tests/testdata/home_data_product_a294.json new file mode 100644 index 00000000..38383bd5 --- /dev/null +++ b/tests/testdata/home_data_product_a294.json @@ -0,0 +1,173 @@ +{ + "model": "roborock.vacuum.a294", + "category": "robot.vacuum.cleaner", + "capability": 0, + "schema": [ + { + "id": 101, + "name": "rpc_request", + "code": "rpc_request", + "mode": "rw", + "type": "RAW" + }, + { + "id": 102, + "name": "rpc_response", + "code": "rpc_response", + "mode": "rw", + "type": "RAW" + }, + { + "id": 120, + "name": "\u9519\u8bef\u4ee3\u7801", + "code": "error_code", + "mode": "ro", + "type": "ENUM", + "property": "{\"range\": [\"\"]}" + }, + { + "id": 121, + "name": "\u8bbe\u5907\u72b6\u6001", + "code": "state", + "mode": "ro", + "type": "ENUM", + "property": "{\"range\": [\"\"]}" + }, + { + "id": 122, + "name": "\u8bbe\u5907\u7535\u91cf", + "code": "battery", + "mode": "ro", + "type": "ENUM", + "property": "{\"range\": [\"\"]}" + }, + { + "id": 123, + "name": "\u6e05\u626b\u6a21\u5f0f", + "code": "fan_power", + "mode": "rw", + "type": "ENUM", + "property": "{\"range\": [\"\"]}" + }, + { + "id": 124, + "name": "\u62d6\u5730\u6a21\u5f0f", + "code": "water_box_mode", + "mode": "rw", + "type": "ENUM", + "property": "{\"range\": [\"\"]}" + }, + { + "id": 125, + "name": "\u4e3b\u5237\u5bff\u547d", + "code": "main_brush_life", + "mode": "rw", + "type": "VALUE", + "property": "{\"max\": 100, \"min\": 0, \"step\": 1, \"unit\": \"null\", \"scale\": 1}" + }, + { + "id": 126, + "name": "\u8fb9\u5237\u5bff\u547d", + "code": "side_brush_life", + "mode": "rw", + "type": "VALUE", + "property": "{\"max\": 100, \"min\": 0, \"step\": 1, \"unit\": \"null\", \"scale\": 1}" + }, + { + "id": 127, + "name": "\u6ee4\u7f51\u5bff\u547d", + "code": "filter_life", + "mode": "rw", + "type": "VALUE", + "property": "{\"max\": 100, \"min\": 0, \"step\": 1, \"unit\": \"null\", \"scale\": 1}" + }, + { + "id": 128, + "name": "\u989d\u5916\u72b6\u6001", + "code": "additional_props", + "mode": "ro", + "type": "RAW" + }, + { + "id": 130, + "name": "\u5b8c\u6210\u4e8b\u4ef6", + "code": "task_complete", + "mode": "ro", + "type": "RAW" + }, + { + "id": 131, + "name": "\u7535\u91cf\u4e0d\u8db3\u4efb\u52a1\u53d6\u6d88", + "code": "task_cancel_low_power", + "mode": "ro", + "type": "RAW" + }, + { + "id": 132, + "name": "\u8fd0\u52a8\u4e2d\u4efb\u52a1\u53d6\u6d88", + "code": "task_cancel_in_motion", + "mode": "ro", + "type": "RAW" + }, + { + "id": 133, + "name": "\u5145\u7535\u72b6\u6001", + "code": "charge_status", + "mode": "ro", + "type": "RAW" + }, + { + "id": 134, + "name": "\u70d8\u5e72\u72b6\u6001", + "code": "drying_status", + "mode": "ro", + "type": "RAW" + }, + { + "id": 135, + "name": "\u79bb\u7ebf\u539f\u56e0\u7ec6\u5206", + "code": "offline_status", + "mode": "ro", + "type": "RAW" + }, + { + "id": 138, + "name": "\u5de5\u4f5c\u4efb\u52a1\u7c7b\u578b", + "code": "clean_task_type", + "mode": "ro", + "type": "ENUM", + "property": "{\"range\": [\"0 \u7a7a\u95f2\", \"1 \u5168\u5c4b\u6e05\u6d01\", \"2 \u9009\u533a\u6e05\u6d01\", \"3 \u5212\u533a\u6e05\u6d01\", \"4 \u5efa\u56fe\", \"5 \u5c40\u90e8\u6e05\u6d01\", \"6 \u9065\u63a7\u6a21\u5f0f\", \"7 \u5de1\u822a\", \"8 \u5bfb\u5ba0\", \"9 \u6574\u7406\"]}" + }, + { + "id": 139, + "name": "\u56de\u57fa\u7ad9\u76ee\u7684", + "code": "back_type", + "mode": "ro", + "type": "RAW" + }, + { + "id": 141, + "name": "\u6e05\u6d01\u8fdb\u5ea6", + "code": "cleaning_progress", + "mode": "ro", + "type": "VALUE", + "property": "{\"max\": 100, \"min\": 0, \"step\": 1, \"unit\": \"\u767e\u5206\u6bd4\", \"scale\": 1}" + }, + { + "id": 142, + "name": "publish_dsp", + "code": "publish_dsp", + "mode": "ro", + "type": "RAW" + }, + { + "id": 143, + "name": "\u5730\u56fe\u53d8\u5316\u6570\u636e", + "code": "map_diff", + "mode": "ro", + "type": "RAW" + } + ], + "id": "product-id-a294", + "name": "Roborock Qrevo 2 Pro Series" +}