From 5216415638149c199663c8c1e82143ba88b5308a Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 13 Sep 2026 11:24:25 -0700 Subject: [PATCH] feat: add Q7 dock state enums --- roborock/data/b01_q7/b01_q7_code_mappings.py | 18 ++++++- roborock/data/b01_q7/b01_q7_containers.py | 6 ++- tests/data/b01_q7/test_b01_q7_containers.py | 51 ++++++++++++++++++++ tests/devices/traits/b01/q7/test_init.py | 17 ++++++- 4 files changed, 88 insertions(+), 4 deletions(-) diff --git a/roborock/data/b01_q7/b01_q7_code_mappings.py b/roborock/data/b01_q7/b01_q7_code_mappings.py index c7426eaaf..df3f8cc0b 100644 --- a/roborock/data/b01_q7/b01_q7_code_mappings.py +++ b/roborock/data/b01_q7/b01_q7_code_mappings.py @@ -1,6 +1,6 @@ from typing import Self, cast -from ..code_mappings import RoborockModeEnum +from ..code_mappings import RoborockEnum, RoborockModeEnum class WorkStatusMapping(RoborockModeEnum): @@ -121,6 +121,22 @@ class WorkModeMapping(RoborockModeEnum): EXPLORE_IDLE = ("explore_idle", 49) +class StationStateMapping(RoborockEnum): + """Known dock activity states observed on the Q7 M5+.""" + + unknown = -1 + idle = 0 + collecting_dust = 3 + + +class DustCollectionStateMapping(RoborockEnum): + """Known dust collection states observed on the Q7 M5+.""" + + unknown = -1 + idle = 0 + collecting_dust = 1 + + class StationActionMapping(RoborockModeEnum): """Maps actions for the cleaning/drying station.""" diff --git a/roborock/data/b01_q7/b01_q7_containers.py b/roborock/data/b01_q7/b01_q7_containers.py index 958d1dc26..144dd84b8 100644 --- a/roborock/data/b01_q7/b01_q7_containers.py +++ b/roborock/data/b01_q7/b01_q7_containers.py @@ -10,7 +10,9 @@ CleanPathPreferenceMapping, CleanRepeatMapping, CleanTypeMapping, + DustCollectionStateMapping, SCWindMapping, + StationStateMapping, WaterLevelMapping, WorkModeMapping, WorkStatusMapping, @@ -137,11 +139,11 @@ class B01Props(RoborockBase): custom_type: int | None = None sound: int | None = None work_mode: WorkModeMapping | None = None - station_act: int | None = None # dock activity; 3 while collecting dust, 0 idle. hw-confirmed (Q7 M5+) + station_act: StationStateMapping | None = None charge_state: int | None = None current_map_id: int | None = None map_num: int | None = None - dust_action: int | None = None # 1 while the dock is collecting dust; read-only (prop.set rejected, code 1) + dust_action: DustCollectionStateMapping | None = None # Read-only; prop.set is rejected with code 1 on the Q7 M5+. quiet_is_open: int | None = None quiet_begin_time: int | None = None quiet_end_time: int | None = None diff --git a/tests/data/b01_q7/test_b01_q7_containers.py b/tests/data/b01_q7/test_b01_q7_containers.py index d2f65ac7c..44515139c 100644 --- a/tests/data/b01_q7/test_b01_q7_containers.py +++ b/tests/data/b01_q7/test_b01_q7_containers.py @@ -12,7 +12,9 @@ CleanRecordDetail, CleanRecordList, CleanRepeatMapping, + DustCollectionStateMapping, SCWindMapping, + StationStateMapping, WorkStatusMapping, ) from roborock.data.code_mappings import completed_warnings @@ -179,6 +181,55 @@ def test_b01props_deserialization_unknown_work_status(caplog: pytest.LogCaptureF assert "Failed to convert status" not in caplog.text +@pytest.mark.parametrize("station_key,dust_key", [("station_act", "dust_action"), ("stationAct", "dustAction")]) +@pytest.mark.parametrize( + ("station_code", "dust_code", "expected_station", "expected_dust"), + [ + (0, 0, StationStateMapping.idle, DustCollectionStateMapping.idle), + (3, 1, StationStateMapping.collecting_dust, DustCollectionStateMapping.collecting_dust), + ], +) +def test_b01props_dock_states( + station_key: str, + dust_key: str, + station_code: int, + dust_code: int, + expected_station: StationStateMapping, + expected_dust: DustCollectionStateMapping, +) -> None: + """Decode dock states and preserve integer values when serializing them.""" + props = B01Props.from_dict({station_key: station_code, dust_key: dust_code}) + + assert props.station_act is expected_station + assert props.dust_action is expected_dust + serialized = json.loads(json.dumps(props.as_dict())) + assert serialized == {"stationAct": station_code, "dustAction": dust_code} + restored = B01Props.from_dict(serialized) + assert restored.station_act is expected_station + assert restored.dust_action is expected_dust + + +@pytest.mark.parametrize("station_code,dust_code", [(-1, -1), (1, 2), (999, 999)]) +def test_b01props_unknown_dock_states(station_code: int, dust_code: int, caplog: pytest.LogCaptureFixture) -> None: + """Unknown dock states must not appear idle or discard other properties.""" + props = B01Props.from_dict({"station_act": station_code, "dust_action": dust_code, "quantity": 87}) + + assert props.station_act is StationStateMapping.unknown + assert props.dust_action is DustCollectionStateMapping.unknown + assert props.quantity == 87 + assert "Failed to convert" not in caplog.text + + +@pytest.mark.parametrize("payload", [{}, {"station_act": None, "dust_action": None}]) +def test_b01props_missing_dock_states(payload: dict[str, None]) -> None: + """Missing dock states stay absent rather than becoming idle or unknown.""" + props = B01Props.from_dict(payload) + + assert props.station_act is None + assert props.dust_action is None + assert props.as_dict() == {} + + def test_b01_q7_clean_record_list_parses_detail_fields(): payload = { "total_time": 34980, diff --git a/tests/devices/traits/b01/q7/test_init.py b/tests/devices/traits/b01/q7/test_init.py index 7707bbe64..9015e1a61 100644 --- a/tests/devices/traits/b01/q7/test_init.py +++ b/tests/devices/traits/b01/q7/test_init.py @@ -5,8 +5,10 @@ from roborock.data.b01_q7 import ( CleanTaskTypeMapping, CleanTypeMapping, + DustCollectionStateMapping, SCDeviceCleanParam, SCWindMapping, + StationStateMapping, WaterLevelMapping, WorkStatusMapping, ) @@ -23,6 +25,8 @@ async def test_q7_api_query_values(q7_api: Q7PropertiesApi, fake_channel: FakeQ7 "status": 1, "wind": 2, "battery": 100, + "station_act": 3, + "dust_action": 1, } fake_channel.response_queue.append(response_data) @@ -31,17 +35,28 @@ async def test_q7_api_query_values(q7_api: Q7PropertiesApi, fake_channel: FakeQ7 [ RoborockB01Props.STATUS, RoborockB01Props.WIND, + RoborockB01Props.STATION_ACT, + RoborockB01Props.DUST_ACTION, ] ) assert result is not None assert result.status == WorkStatusMapping.WAITING_FOR_ORDERS assert result.wind == SCWindMapping.STANDARD + assert result.station_act is StationStateMapping.collecting_dust + assert result.dust_action is DustCollectionStateMapping.collecting_dust assert len(fake_channel.published_commands) == 1 command, params = fake_channel.published_commands[0] assert command == RoborockB01Q7Methods.GET_PROP - assert params == {"property": [RoborockB01Props.STATUS, RoborockB01Props.WIND]} + assert params == { + "property": [ + RoborockB01Props.STATUS, + RoborockB01Props.WIND, + RoborockB01Props.STATION_ACT, + RoborockB01Props.DUST_ACTION, + ] + } @pytest.mark.parametrize(