From 0ed8729d0f80c983920b7aa20a2062d72ba4928e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 4 Apr 2026 10:42:53 +0200 Subject: [PATCH 1/4] feat: update schema requirements in StatusV2 Update the StatusV2 to check the schema for supported fields. --- roborock/data/v1/v1_containers.py | 15 ++++++++------- .../v1/__snapshots__/test_device_features.ambr | 3 +++ tests/devices/traits/v1/test_device_features.py | 6 ++++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/roborock/data/v1/v1_containers.py b/roborock/data/v1/v1_containers.py index cf988590..24ac4581 100644 --- a/roborock/data/v1/v1_containers.py +++ b/roborock/data/v1/v1_containers.py @@ -113,6 +113,7 @@ class StatusField(FieldNameBase): WATER_BOX_MODE = "water_box_mode" CHARGE_STATUS = "charge_status" DRY_STATUS = "dry_status" + ERROR_CODE = "error_code" def _requires_schema_code(requires_schema_code: str, default=None) -> Any: @@ -293,11 +294,11 @@ class StatusV2(RoborockBase): msg_ver: int | None = None msg_seq: int | None = None - state: RoborockStateCode | None = None - battery: int | None = None + state: RoborockStateCode | None = field(metadata={"requires_schema_code": "state"}, default=None) + battery: int | None = field(metadata={"requires_schema_code": "battery"}, default=None) clean_time: int | None = None clean_area: int | None = None - error_code: RoborockErrorCode | None = None + error_code: RoborockErrorCode | None = field(metadata={"requires_schema_code": "error_code"}, default=None) map_present: int | None = None in_cleaning: RoborockInCleaning | None = None in_returning: int | None = None @@ -307,12 +308,12 @@ class StatusV2(RoborockBase): back_type: int | None = None wash_phase: int | None = None wash_ready: int | None = None - fan_power: int | None = None + fan_power: int | None = field(metadata={"requires_schema_code": "fan_power"}, default=None) dnd_enabled: int | None = None map_status: int | None = None is_locating: int | None = None lock_status: int | None = None - water_box_mode: int | None = None + water_box_mode: int | None = field(metadata={"requires_schema_code": "water_box_mode"}, default=None) water_box_carriage_status: int | None = None mop_forbidden_enable: int | None = None camera_status: int | None = None @@ -330,13 +331,13 @@ class StatusV2(RoborockBase): collision_avoid_status: int | None = None switch_map_mode: int | None = None dock_error_status: RoborockDockErrorCode | None = None - charge_status: int | None = None + charge_status: int | None = field(metadata={"requires_schema_code": "charge_status"}, default=None) unsave_map_reason: int | None = None unsave_map_flag: int | None = None wash_status: int | None = None distance_off: int | None = None in_warmup: int | None = None - dry_status: int | None = None + dry_status: int | None = field(metadata={"requires_schema_code": "drying_status"}, default=None) rdt: int | None = None clean_percent: int | None = None rss: int | None = None diff --git a/tests/devices/traits/v1/__snapshots__/test_device_features.ambr b/tests/devices/traits/v1/__snapshots__/test_device_features.ambr index 9b2a4827..de765936 100644 --- a/tests/devices/traits/v1/__snapshots__/test_device_features.ambr +++ b/tests/devices/traits/v1/__snapshots__/test_device_features.ambr @@ -4,6 +4,7 @@ 'battery': True, 'charge_status': True, 'dry_status': True, + 'error_code': True, 'fan_power': True, 'state': True, 'water_box_mode': True, @@ -14,6 +15,7 @@ 'battery': True, 'charge_status': True, 'dry_status': True, + 'error_code': True, 'fan_power': True, 'state': True, 'water_box_mode': True, @@ -24,6 +26,7 @@ 'battery': True, 'charge_status': True, 'dry_status': True, + 'error_code': True, 'fan_power': True, 'state': True, 'water_box_mode': True, diff --git a/tests/devices/traits/v1/test_device_features.py b/tests/devices/traits/v1/test_device_features.py index f4deb52e..b4535c8e 100644 --- a/tests/devices/traits/v1/test_device_features.py +++ b/tests/devices/traits/v1/test_device_features.py @@ -29,5 +29,7 @@ async def test_is_attribute_supported( assert device.v1_properties.device_features is not None device_features_trait = device.v1_properties.device_features - is_supported = {field.value: device_features_trait.is_field_supported(StatusTrait, field) for field in StatusField} - assert is_supported == snapshot + is_v1_supported = { + field.value: device_features_trait.is_field_supported(StatusTrait, field) for field in StatusField + } + assert is_v1_supported == snapshot From ebf3d6220099b2fe1e578a4c1978f1e6bf223ec2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 4 Apr 2026 12:26:32 +0200 Subject: [PATCH 2/4] fix: update error_code and other fields to use _requires_schema_code for schema validation --- roborock/data/v1/v1_containers.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/roborock/data/v1/v1_containers.py b/roborock/data/v1/v1_containers.py index 24ac4581..f9b5082d 100644 --- a/roborock/data/v1/v1_containers.py +++ b/roborock/data/v1/v1_containers.py @@ -130,7 +130,7 @@ class Status(RoborockBase): battery: int | None = _requires_schema_code("battery", default=None) clean_time: int | None = None clean_area: int | None = None - error_code: RoborockErrorCode | None = None + error_code: RoborockErrorCode | None = _requires_schema_code("error_code", default=None) map_present: int | None = None in_cleaning: RoborockInCleaning | None = None in_returning: int | None = None @@ -294,11 +294,11 @@ class StatusV2(RoborockBase): msg_ver: int | None = None msg_seq: int | None = None - state: RoborockStateCode | None = field(metadata={"requires_schema_code": "state"}, default=None) - battery: int | None = field(metadata={"requires_schema_code": "battery"}, default=None) + state: RoborockStateCode | None = _requires_schema_code("state", default=None) + battery: int | None = _requires_schema_code("battery", default=None) clean_time: int | None = None clean_area: int | None = None - error_code: RoborockErrorCode | None = field(metadata={"requires_schema_code": "error_code"}, default=None) + error_code: RoborockErrorCode | None = _requires_schema_code("error_code", default=None) map_present: int | None = None in_cleaning: RoborockInCleaning | None = None in_returning: int | None = None @@ -308,12 +308,12 @@ class StatusV2(RoborockBase): back_type: int | None = None wash_phase: int | None = None wash_ready: int | None = None - fan_power: int | None = field(metadata={"requires_schema_code": "fan_power"}, default=None) + fan_power: int | None = _requires_schema_code("fan_power", default=None) dnd_enabled: int | None = None map_status: int | None = None is_locating: int | None = None lock_status: int | None = None - water_box_mode: int | None = field(metadata={"requires_schema_code": "water_box_mode"}, default=None) + water_box_mode: int | None = _requires_schema_code("water_box_mode", default=None) water_box_carriage_status: int | None = None mop_forbidden_enable: int | None = None camera_status: int | None = None @@ -330,14 +330,14 @@ class StatusV2(RoborockBase): debug_mode: int | None = None collision_avoid_status: int | None = None switch_map_mode: int | None = None - dock_error_status: RoborockDockErrorCode | None = None - charge_status: int | None = field(metadata={"requires_schema_code": "charge_status"}, default=None) + dock_error_status: RoborockDockErrorCode | None = _requires_schema_code("dock_error_status", default=None) + charge_status: int | None = _requires_schema_code("charge_status", default=None) unsave_map_reason: int | None = None unsave_map_flag: int | None = None wash_status: int | None = None distance_off: int | None = None in_warmup: int | None = None - dry_status: int | None = field(metadata={"requires_schema_code": "drying_status"}, default=None) + dry_status: int | None = _requires_schema_code("drying_status", default=None) rdt: int | None = None clean_percent: int | None = None rss: int | None = None From 1a188d9039bfd5e09cfc7fce6e4c3d40a13a5a47 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 4 Apr 2026 12:38:49 +0200 Subject: [PATCH 3/4] feat: Add consumable fields --- roborock/data/v1/v1_containers.py | 22 ++++++++++++++--- .../__snapshots__/test_device_features.ambr | 21 ++++++++++++++++ .../devices/traits/v1/test_device_features.py | 24 ++++++++++++++++++- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/roborock/data/v1/v1_containers.py b/roborock/data/v1/v1_containers.py index f9b5082d..58f4e0b4 100644 --- a/roborock/data/v1/v1_containers.py +++ b/roborock/data/v1/v1_containers.py @@ -625,11 +625,27 @@ class CleanSummaryWithDetail(CleanSummary): last_clean_record: CleanRecord | None = None +class ConsumableField(FieldNameBase): + """An enum that represents a field in the `Consumable` class. + + This is used with `roborock.devices.traits.v1.status.DeviceFeaturesTrait` + to understand if a feature is supported by the device using `is_field_supported`. + + The enum values are names of fields in the `Consumable` class. Each field is + annotated with `requires_schema_code` metadata to map the field to a schema + code in the product schema, which may have a different name than the field/attribute name. + """ + + MAIN_BRUSH_WORK_TIME = "main_brush_work_time" + SIDE_BRUSH_WORK_TIME = "side_brush_work_time" + FILTER_WORK_TIME = "filter_work_time" + + @dataclass class Consumable(RoborockBase): - main_brush_work_time: int | None = None - side_brush_work_time: int | None = None - filter_work_time: int | None = None + main_brush_work_time: int | None = field(metadata={"requires_schema_code": "main_brush_life"}, default=None) + side_brush_work_time: int | None = field(metadata={"requires_schema_code": "side_brush_life"}, default=None) + filter_work_time: int | None = field(metadata={"requires_schema_code": "filter_life"}, default=None) filter_element_work_time: int | None = None sensor_dirty_time: int | None = None strainer_work_times: int | None = None diff --git a/tests/devices/traits/v1/__snapshots__/test_device_features.ambr b/tests/devices/traits/v1/__snapshots__/test_device_features.ambr index de765936..71313f0a 100644 --- a/tests/devices/traits/v1/__snapshots__/test_device_features.ambr +++ b/tests/devices/traits/v1/__snapshots__/test_device_features.ambr @@ -32,3 +32,24 @@ 'water_box_mode': True, }) # --- +# name: test_is_consumable_field_supported[home_data_device_s5e.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_s7_maxv.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_saros_10r.json] + dict({ + 'filter_work_time': True, + 'main_brush_work_time': True, + 'side_brush_work_time': True, + }) +# --- diff --git a/tests/devices/traits/v1/test_device_features.py b/tests/devices/traits/v1/test_device_features.py index b4535c8e..7beb529d 100644 --- a/tests/devices/traits/v1/test_device_features.py +++ b/tests/devices/traits/v1/test_device_features.py @@ -4,8 +4,9 @@ from syrupy import SnapshotAssertion from roborock.data import HomeDataDevice -from roborock.data.v1.v1_containers import StatusField +from roborock.data.v1.v1_containers import ConsumableField, StatusField from roborock.devices.device import RoborockDevice +from roborock.devices.traits.v1.consumeable import ConsumableTrait from roborock.devices.traits.v1.status import StatusTrait from tests import mock_data @@ -33,3 +34,24 @@ async def test_is_attribute_supported( field.value: device_features_trait.is_field_supported(StatusTrait, field) for field in StatusField } assert is_v1_supported == snapshot + + +@pytest.mark.parametrize( + ("device_info"), + V1_DEVICES.values(), + ids=list(V1_DEVICES.keys()), +) +async def test_is_consumable_field_supported( + device_info: HomeDataDevice, + device: RoborockDevice, + snapshot: SnapshotAssertion, +) -> None: + """Test if a field is supported.""" + assert device.v1_properties is not None + assert device.v1_properties.device_features is not None + device_features_trait = device.v1_properties.device_features + + is_v1_supported = { + field.value: device_features_trait.is_field_supported(ConsumableTrait, field) for field in ConsumableField + } + assert is_v1_supported == snapshot From 471f362193a278f6f206737a94596020a2428c29 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 4 Apr 2026 22:03:00 -0700 Subject: [PATCH 4/4] refactor: remove redundant default=None arguments from _requires_schema_code calls in v1_containers --- roborock/data/v1/v1_containers.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/roborock/data/v1/v1_containers.py b/roborock/data/v1/v1_containers.py index 58f4e0b4..3066d1a3 100644 --- a/roborock/data/v1/v1_containers.py +++ b/roborock/data/v1/v1_containers.py @@ -126,11 +126,11 @@ class Status(RoborockBase): msg_ver: int | None = None msg_seq: int | None = None - state: RoborockStateCode | None = _requires_schema_code("state", default=None) - battery: int | None = _requires_schema_code("battery", default=None) + state: RoborockStateCode | None = _requires_schema_code("state") + battery: int | None = _requires_schema_code("battery") clean_time: int | None = None clean_area: int | None = None - error_code: RoborockErrorCode | None = _requires_schema_code("error_code", default=None) + error_code: RoborockErrorCode | None = _requires_schema_code("error_code") map_present: int | None = None in_cleaning: RoborockInCleaning | None = None in_returning: int | None = None @@ -140,12 +140,12 @@ class Status(RoborockBase): back_type: int | None = None wash_phase: int | None = None wash_ready: int | None = None - fan_power: RoborockFanPowerCode | None = _requires_schema_code("fan_power", default=None) + fan_power: RoborockFanPowerCode | None = _requires_schema_code("fan_power") dnd_enabled: int | None = None map_status: int | None = None is_locating: int | None = None lock_status: int | None = None - water_box_mode: RoborockMopIntensityCode | None = _requires_schema_code("water_box_mode", default=None) + water_box_mode: RoborockMopIntensityCode | None = _requires_schema_code("water_box_mode") water_box_carriage_status: int | None = None mop_forbidden_enable: int | None = None camera_status: int | None = None @@ -163,13 +163,13 @@ class Status(RoborockBase): collision_avoid_status: int | None = None switch_map_mode: int | None = None dock_error_status: RoborockDockErrorCode | None = None - charge_status: int | None = _requires_schema_code("charge_status", default=None) + charge_status: int | None = _requires_schema_code("charge_status") unsave_map_reason: int | None = None unsave_map_flag: int | None = None wash_status: int | None = None distance_off: int | None = None in_warmup: int | None = None - dry_status: int | None = _requires_schema_code("drying_status", default=None) + dry_status: int | None = _requires_schema_code("drying_status") rdt: int | None = None clean_percent: int | None = None rss: int | None = None @@ -294,11 +294,11 @@ class StatusV2(RoborockBase): msg_ver: int | None = None msg_seq: int | None = None - state: RoborockStateCode | None = _requires_schema_code("state", default=None) - battery: int | None = _requires_schema_code("battery", default=None) + state: RoborockStateCode | None = _requires_schema_code("state") + battery: int | None = _requires_schema_code("battery") clean_time: int | None = None clean_area: int | None = None - error_code: RoborockErrorCode | None = _requires_schema_code("error_code", default=None) + error_code: RoborockErrorCode | None = _requires_schema_code("error_code") map_present: int | None = None in_cleaning: RoborockInCleaning | None = None in_returning: int | None = None @@ -308,12 +308,12 @@ class StatusV2(RoborockBase): back_type: int | None = None wash_phase: int | None = None wash_ready: int | None = None - fan_power: int | None = _requires_schema_code("fan_power", default=None) + fan_power: int | None = _requires_schema_code("fan_power") dnd_enabled: int | None = None map_status: int | None = None is_locating: int | None = None lock_status: int | None = None - water_box_mode: int | None = _requires_schema_code("water_box_mode", default=None) + water_box_mode: int | None = _requires_schema_code("water_box_mode") water_box_carriage_status: int | None = None mop_forbidden_enable: int | None = None camera_status: int | None = None @@ -330,14 +330,14 @@ class StatusV2(RoborockBase): debug_mode: int | None = None collision_avoid_status: int | None = None switch_map_mode: int | None = None - dock_error_status: RoborockDockErrorCode | None = _requires_schema_code("dock_error_status", default=None) - charge_status: int | None = _requires_schema_code("charge_status", default=None) + dock_error_status: RoborockDockErrorCode | None = _requires_schema_code("dock_error_status") + charge_status: int | None = _requires_schema_code("charge_status") unsave_map_reason: int | None = None unsave_map_flag: int | None = None wash_status: int | None = None distance_off: int | None = None in_warmup: int | None = None - dry_status: int | None = _requires_schema_code("drying_status", default=None) + dry_status: int | None = _requires_schema_code("drying_status") rdt: int | None = None clean_percent: int | None = None rss: int | None = None