From bad5564ab48cbe43942da60c3576683006f79265 Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Fri, 3 Jul 2026 16:02:50 -0400 Subject: [PATCH 01/12] feat: decode Q10 (B01/ss07) dpFault codes into a YXFault map Add a Q10-specific YXFault enum (seeded from the ss07 app fault i18n) and an additive Q10Status.fault_name property. The raw fault int is untouched, so unknown/new codes are never lost. Addresses #855. --- .../data/b01_q10/b01_q10_code_mappings.py | 58 +++++++++++++++++++ roborock/data/b01_q10/b01_q10_containers.py | 14 +++++ .../b01_q10/test_b01_q10_code_mappings.py | 29 +++++++++- 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index 78fc1b2c..1427770e 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -289,6 +289,64 @@ class YXCarpetCleanType(RoborockModeEnum): CROSS = "cross", 3 +class YXFault(RoborockModeEnum): + """Q10 (B01/ss07) ``dpFault`` (90) codes, seeded from the ss07 app's fault i18n. + + ``dpFault`` is *overloaded*: several values are lifecycle/status rather than + errors (e.g. 400 = scheduled clean starting, 501 = returning to dock, + 502 = recharge). A non-zero fault is only a *blocking* error when the app + routes the code into its ``D_ErrorCode`` bucket; alert/offline codes are softer. + + These labels follow the ss07 app text, which differs from the Q7 ``B01Fault`` + for several shared numbers (500, 501, 503, 569, 570) -- so this is a + Q10-specific map, not a reuse of ``B01Fault``. Codes marked "seen live" were + observed as raw ``dpFault`` values on a physical ss07; the rest are + app-sourced only (n=1, not hardware-confirmed). + """ + + UNKNOWN = "unknown", -1 + NONE = "none", 0 + LIDAR_BLOCKED = "lidar_blocked", 1 + BUMPER_STUCK = "bumper_stuck", 2 + ROBOT_SUSPENDED = "robot_suspended", 3 + CLIFF_SENSOR_ERROR = "cliff_sensor_error", 4 + MAIN_BRUSH_STUCK = "main_brush_stuck", 5 + MAIN_WHEELS_STUCK = "main_wheels_stuck", 7 + ROBOT_TRAPPED = "robot_trapped", 8 # seen live + CHECK_DUSTBIN_FILTER = "check_dustbin_filter", 9 + LOW_BATTERY = "low_battery", 12 + TEMPERATURE_THRESHOLD = "temperature_threshold", 14 + ROBOT_TILTED = "robot_tilted", 16 + LIDAR_COVER_OBSTRUCTED = "lidar_cover_obstructed", 21 + NO_GO_ZONE_DETECTED = "no_go_zone_detected", 24 + MOPPING_MODULE_STUCK = "mopping_module_stuck", 27 + CARPET_AVOIDANCE = "carpet_avoidance", 28 + CANNOT_CROSS_CARPET = "cannot_cross_carpet", 29 + INSTALL_DUST_BAG = "install_dust_bag", 46 + MOP_MOUNT_FELL_OFF = "mop_mount_fell_off", 54 + LIDAR_DIRTY = "lidar_dirty", 58 + FILTER_SERVICE_LIFE = "filter_service_life", 301 + MAIN_BRUSH_SERVICE_LIFE = "main_brush_service_life", 302 + SIDE_BRUSH_SERVICE_LIFE = "side_brush_service_life", 303 + SENSOR_NEEDS_CLEANING = "sensor_needs_cleaning", 304 + DUST_BAG_FULL = "dust_bag_full", 310 + STARTING_SCHEDULED_CLEAN = "starting_scheduled_clean", 400 # seen live; lifecycle, not an error + CLEANING_IN_PROGRESS = "cleaning_in_progress", 407 + EMPTY_DUSTBIN = "empty_dustbin", 500 # ss07 != Q7 B01Fault (lidar_blocked) + CLEANING_COMPLETED_RETURNING = "cleaning_completed_returning", 501 # ss07 != Q7 (robot_suspended) + LOW_BATTERY_RESUME = "low_battery_resume", 502 + DOCKING_ERROR = "docking_error", 503 # seen live; ss07 != Q7 (dustbin_not_installed) + POSITIONING_FAILED = "positioning_failed", 556 # seen live; relocalization + TOO_FREQUENT_EMPTYING = "too_frequent_emptying", 569 # ss07 != Q7 (main_wheels_entangled) + CANNOT_REACH_TARGET = "cannot_reach_target", 570 # seen live; ss07 != Q7 (main_brush_entangled) + OFFLINE_WARNING_ASLEEP = "offline_warning_asleep", 588 + OFFLINE_WARNING_LOW_BATTERY = "offline_warning_low_battery", 589 + DND_AUTO_TOPUP_DISABLED = "dnd_auto_topup_disabled", 591 + CLEAN_CARPET_ULTRASONIC_SENSORS = "clean_carpet_ultrasonic_sensors", 707 + ROBOT_ERROR_RESET = "robot_error_reset", 1002 + VOICE_PACK_UPDATE_AVAILABLE = "voice_pack_update_available", 3001 + + class RemoteCommand(IntEnum): FORWARD = 0 LEFT = 2 diff --git a/roborock/data/b01_q10/b01_q10_containers.py b/roborock/data/b01_q10/b01_q10_containers.py index 7b0351a6..bd28d92e 100644 --- a/roborock/data/b01_q10/b01_q10_containers.py +++ b/roborock/data/b01_q10/b01_q10_containers.py @@ -23,6 +23,7 @@ YXDeviceDustCollectionFrequency, YXDeviceState, YXFanLevel, + YXFault, YXStartMethod, YXWaterLevel, ) @@ -203,6 +204,19 @@ class Q10Status(RoborockBase): filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE}) sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE}) + @property + def fault_name(self) -> YXFault | None: + """Decoded ``dpFault`` label, or ``None`` for an unmapped code. + + Additive: ``fault`` keeps the raw integer so unknown/new codes are never + lost; this is the decoded companion. ``dpFault`` is overloaded (see + :class:`~roborock.data.b01_q10.YXFault`), so a non-``NONE`` value is not + necessarily an error. + """ + if self.fault is None: + return None + return YXFault.from_code_optional(self.fault) + @dataclass class SoundVolume(RoborockBase): diff --git a/tests/data/b01_q10/test_b01_q10_code_mappings.py b/tests/data/b01_q10/test_b01_q10_code_mappings.py index 63fdd14d..d6002884 100644 --- a/tests/data/b01_q10/test_b01_q10_code_mappings.py +++ b/tests/data/b01_q10/test_b01_q10_code_mappings.py @@ -2,7 +2,7 @@ import pytest -from roborock.data.b01_q10 import YXDeviceState +from roborock.data.b01_q10 import Q10Status, YXDeviceState, YXFault @pytest.mark.parametrize( @@ -33,3 +33,30 @@ def test_q10_status_values_are_canonical(state: YXDeviceState, string: str) -> N def test_q10_status_codes_map_to_canonical_values(code: int, expected_state: YXDeviceState) -> None: """Code-based mapping should return canonical status values.""" assert YXDeviceState.from_code(code) is expected_state + + +@pytest.mark.parametrize( + "code, expected", + [ + (0, YXFault.NONE), + (503, YXFault.DOCKING_ERROR), # ss07 docking error, not the Q7 "dustbin_not_installed" + (570, YXFault.CANNOT_REACH_TARGET), # ss07 cannot-reach, not the Q7 "main_brush_entangled" + (556, YXFault.POSITIONING_FAILED), + ], +) +def test_q10_fault_codes_map_to_ss07_labels(code: int, expected: YXFault) -> None: + """dpFault codes should decode to the ss07-correct labels.""" + assert YXFault.from_code(code) is expected + + +def test_q10_fault_name_is_additive_and_preserves_raw_int() -> None: + """``fault_name`` decodes the label without ever losing the raw ``fault`` int.""" + status = Q10Status(fault=570) + assert status.fault_name is YXFault.CANNOT_REACH_TARGET + assert status.fault == 570 + + unmapped = Q10Status(fault=99999) + assert unmapped.fault_name is None # unknown code -> None, no crash + assert unmapped.fault == 99999 + + assert Q10Status(fault=None).fault_name is None From 4c97af980df2381ab3662a72583446fbddd83e86 Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Fri, 3 Jul 2026 20:30:39 -0400 Subject: [PATCH 02/12] refactor: type Q10Status.fault as YXFault, mirroring the Q7 fault field Retype the fault field from a raw int to YXFault | None and make fault_name a str accessor, matching B01Props.fault / fault_name on the Q7. Update the enum provenance notes from live hardware confirmation (10 codes) and drop the unconfirmed 569 label. --- .../data/b01_q10/b01_q10_code_mappings.py | 36 +++++++++---------- roborock/data/b01_q10/b01_q10_containers.py | 16 +++------ .../b01_q10/test_b01_q10_code_mappings.py | 28 ++++++++++----- tests/devices/traits/b01/q10/test_status.py | 3 +- 4 files changed, 42 insertions(+), 41 deletions(-) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index 1427770e..e6bf410c 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -294,25 +294,24 @@ class YXFault(RoborockModeEnum): ``dpFault`` is *overloaded*: several values are lifecycle/status rather than errors (e.g. 400 = scheduled clean starting, 501 = returning to dock, - 502 = recharge). A non-zero fault is only a *blocking* error when the app - routes the code into its ``D_ErrorCode`` bucket; alert/offline codes are softer. + 502 = recharge). A non-zero fault is not necessarily a blocking error. These labels follow the ss07 app text, which differs from the Q7 ``B01Fault`` - for several shared numbers (500, 501, 503, 569, 570) -- so this is a - Q10-specific map, not a reuse of ``B01Fault``. Codes marked "seen live" were - observed as raw ``dpFault`` values on a physical ss07; the rest are - app-sourced only (n=1, not hardware-confirmed). + for several shared numbers (500, 501, 503, 570) -- so this is a Q10-specific + map, not a reuse of ``B01Fault``. Codes marked "hw-confirmed" were observed + firing live on a physical ss07 in a context matching the label; the rest are + app-sourced only (single device, single app version). """ UNKNOWN = "unknown", -1 NONE = "none", 0 LIDAR_BLOCKED = "lidar_blocked", 1 BUMPER_STUCK = "bumper_stuck", 2 - ROBOT_SUSPENDED = "robot_suspended", 3 - CLIFF_SENSOR_ERROR = "cliff_sensor_error", 4 + ROBOT_SUSPENDED = "robot_suspended", 3 # hw-confirmed (wheels lifted off the floor mid-task) + CLIFF_SENSOR_ERROR = "cliff_sensor_error", 4 # hw-confirmed (one side lifted mid-task) MAIN_BRUSH_STUCK = "main_brush_stuck", 5 MAIN_WHEELS_STUCK = "main_wheels_stuck", 7 - ROBOT_TRAPPED = "robot_trapped", 8 # seen live + ROBOT_TRAPPED = "robot_trapped", 8 # hw-confirmed CHECK_DUSTBIN_FILTER = "check_dustbin_filter", 9 LOW_BATTERY = "low_battery", 12 TEMPERATURE_THRESHOLD = "temperature_threshold", 14 @@ -328,17 +327,16 @@ class YXFault(RoborockModeEnum): FILTER_SERVICE_LIFE = "filter_service_life", 301 MAIN_BRUSH_SERVICE_LIFE = "main_brush_service_life", 302 SIDE_BRUSH_SERVICE_LIFE = "side_brush_service_life", 303 - SENSOR_NEEDS_CLEANING = "sensor_needs_cleaning", 304 - DUST_BAG_FULL = "dust_bag_full", 310 - STARTING_SCHEDULED_CLEAN = "starting_scheduled_clean", 400 # seen live; lifecycle, not an error + SENSOR_NEEDS_CLEANING = "sensor_needs_cleaning", 304 # hw-confirmed (fired during auto-empty) + DUST_BAG_FULL = "dust_bag_full", 310 # inferred from auto-empty context; not hw-confirmed + STARTING_SCHEDULED_CLEAN = "starting_scheduled_clean", 400 # hw-confirmed x3; lifecycle, not an error CLEANING_IN_PROGRESS = "cleaning_in_progress", 407 - EMPTY_DUSTBIN = "empty_dustbin", 500 # ss07 != Q7 B01Fault (lidar_blocked) - CLEANING_COMPLETED_RETURNING = "cleaning_completed_returning", 501 # ss07 != Q7 (robot_suspended) - LOW_BATTERY_RESUME = "low_battery_resume", 502 - DOCKING_ERROR = "docking_error", 503 # seen live; ss07 != Q7 (dustbin_not_installed) - POSITIONING_FAILED = "positioning_failed", 556 # seen live; relocalization - TOO_FREQUENT_EMPTYING = "too_frequent_emptying", 569 # ss07 != Q7 (main_wheels_entangled) - CANNOT_REACH_TARGET = "cannot_reach_target", 570 # seen live; ss07 != Q7 (main_brush_entangled) + EMPTY_DUSTBIN = "empty_dustbin", 500 # ss07 != Q7 B01Fault (lidar_blocked); app-sourced + CLEANING_COMPLETED_RETURNING = "cleaning_completed_returning", 501 # hw-confirmed; ss07 != Q7 (robot_suspended) + LOW_BATTERY_RESUME = "low_battery_resume", 502 # hw-confirmed; lifecycle + DOCKING_ERROR = "docking_error", 503 # hw-confirmed; ss07 != Q7 (dustbin_not_installed) + POSITIONING_FAILED = "positioning_failed", 556 # hw-confirmed; relocalization + CANNOT_REACH_TARGET = "cannot_reach_target", 570 # hw-confirmed; ss07 != Q7 (main_brush_entangled) OFFLINE_WARNING_ASLEEP = "offline_warning_asleep", 588 OFFLINE_WARNING_LOW_BATTERY = "offline_warning_low_battery", 589 DND_AUTO_TOPUP_DISABLED = "dnd_auto_topup_disabled", 591 diff --git a/roborock/data/b01_q10/b01_q10_containers.py b/roborock/data/b01_q10/b01_q10_containers.py index bd28d92e..2223cc86 100644 --- a/roborock/data/b01_q10/b01_q10_containers.py +++ b/roborock/data/b01_q10/b01_q10_containers.py @@ -166,7 +166,7 @@ class Q10Status(RoborockBase): clean_task_type: YXDeviceCleanTask | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TASK_TYPE}) back_type: YXBackType | None = field(default=None, metadata={"dps": B01_Q10_DP.BACK_TYPE}) cleaning_progress: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_PROGRESS}) - fault: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FAULT}) + fault: YXFault | None = field(default=None, metadata={"dps": B01_Q10_DP.FAULT}) # Additional state reported in the device's full status dump. clean_line: YXCleanLine | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_LINE}) @@ -205,17 +205,9 @@ class Q10Status(RoborockBase): sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE}) @property - def fault_name(self) -> YXFault | None: - """Decoded ``dpFault`` label, or ``None`` for an unmapped code. - - Additive: ``fault`` keeps the raw integer so unknown/new codes are never - lost; this is the decoded companion. ``dpFault`` is overloaded (see - :class:`~roborock.data.b01_q10.YXFault`), so a non-``NONE`` value is not - necessarily an error. - """ - if self.fault is None: - return None - return YXFault.from_code_optional(self.fault) + def fault_name(self) -> str | None: + """Returns the name of the current fault.""" + return self.fault.value if self.fault is not None else None @dataclass diff --git a/tests/data/b01_q10/test_b01_q10_code_mappings.py b/tests/data/b01_q10/test_b01_q10_code_mappings.py index d6002884..06789a23 100644 --- a/tests/data/b01_q10/test_b01_q10_code_mappings.py +++ b/tests/data/b01_q10/test_b01_q10_code_mappings.py @@ -49,14 +49,24 @@ def test_q10_fault_codes_map_to_ss07_labels(code: int, expected: YXFault) -> Non assert YXFault.from_code(code) is expected -def test_q10_fault_name_is_additive_and_preserves_raw_int() -> None: - """``fault_name`` decodes the label without ever losing the raw ``fault`` int.""" - status = Q10Status(fault=570) - assert status.fault_name is YXFault.CANNOT_REACH_TARGET - assert status.fault == 570 - - unmapped = Q10Status(fault=99999) - assert unmapped.fault_name is None # unknown code -> None, no crash - assert unmapped.fault == 99999 +def test_q10_status_fault_field_and_name() -> None: + """``fault`` is a typed ``YXFault`` field; ``fault_name`` mirrors the Q7 accessor.""" + status = Q10Status(fault=YXFault.CANNOT_REACH_TARGET) + assert status.fault is YXFault.CANNOT_REACH_TARGET + assert status.fault_name == "cannot_reach_target" assert Q10Status(fault=None).fault_name is None + + +def test_q10_status_fault_decodes_from_dict() -> None: + """A raw dpFault int decodes to the enum through the standard conversion path.""" + status = Q10Status.from_dict({"fault": 503}) + assert status is not None + assert status.fault is YXFault.DOCKING_ERROR + + # An unmapped code follows the library-wide enum-field behavior: + # the failed conversion is logged and the field is left None. + unmapped = Q10Status.from_dict({"fault": 99999}) + assert unmapped is not None + assert unmapped.fault is None + assert unmapped.fault_name is None diff --git a/tests/devices/traits/b01/q10/test_status.py b/tests/devices/traits/b01/q10/test_status.py index 8105142a..edd4bcb1 100644 --- a/tests/devices/traits/b01/q10/test_status.py +++ b/tests/devices/traits/b01/q10/test_status.py @@ -18,6 +18,7 @@ YXDeviceDustCollectionFrequency, YXDeviceState, YXFanLevel, + YXFault, YXWaterLevel, ) from roborock.data.b01_q10.b01_q10_containers import dpNetInfo, dpNotDisturbExpand, dpTimeZone @@ -150,7 +151,7 @@ async def test_status_trait_refresh( assert q10_api.consumable.filter_life == 0 assert q10_api.consumable.sensor_life == 0 assert q10_api.status.cleaning_progress == 100 - assert q10_api.status.fault == 0 + assert q10_api.status.fault is YXFault.NONE assert q10_api.status.clean_mode == YXCleanType.VAC_AND_MOP assert q10_api.status.water_level == YXWaterLevel.LOW From afcca0ff9a0235ac3a22efeed9f49176a019d432 Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Sat, 4 Jul 2026 17:58:13 -0400 Subject: [PATCH 03/12] docs: note fault 501 fires per completed task --- roborock/data/b01_q10/b01_q10_code_mappings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index e6bf410c..cee341d1 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -332,7 +332,8 @@ class YXFault(RoborockModeEnum): STARTING_SCHEDULED_CLEAN = "starting_scheduled_clean", 400 # hw-confirmed x3; lifecycle, not an error CLEANING_IN_PROGRESS = "cleaning_in_progress", 407 EMPTY_DUSTBIN = "empty_dustbin", 500 # ss07 != Q7 B01Fault (lidar_blocked); app-sourced - CLEANING_COMPLETED_RETURNING = "cleaning_completed_returning", 501 # hw-confirmed; ss07 != Q7 (robot_suspended) + # hw-confirmed, fires per completed task; ss07 != Q7 (robot_suspended) + CLEANING_COMPLETED_RETURNING = "cleaning_completed_returning", 501 LOW_BATTERY_RESUME = "low_battery_resume", 502 # hw-confirmed; lifecycle DOCKING_ERROR = "docking_error", 503 # hw-confirmed; ss07 != Q7 (dustbin_not_installed) POSITIONING_FAILED = "positioning_failed", 556 # hw-confirmed; relocalization From e0400de3d839b4d86b9feb8152da9af095e6a317 Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Sat, 4 Jul 2026 18:02:07 -0400 Subject: [PATCH 04/12] docs: mark fault 5 hw-confirmed (physically ground-truthed brush jams) --- roborock/data/b01_q10/b01_q10_code_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index cee341d1..abc02236 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -309,7 +309,7 @@ class YXFault(RoborockModeEnum): BUMPER_STUCK = "bumper_stuck", 2 ROBOT_SUSPENDED = "robot_suspended", 3 # hw-confirmed (wheels lifted off the floor mid-task) CLIFF_SENSOR_ERROR = "cliff_sensor_error", 4 # hw-confirmed (one side lifted mid-task) - MAIN_BRUSH_STUCK = "main_brush_stuck", 5 + MAIN_BRUSH_STUCK = "main_brush_stuck", 5 # hw-confirmed (two physical brush jams) MAIN_WHEELS_STUCK = "main_wheels_stuck", 7 ROBOT_TRAPPED = "robot_trapped", 8 # hw-confirmed CHECK_DUSTBIN_FILTER = "check_dustbin_filter", 9 From cbf439974fe747a7977b83b84084f98a70fa2a77 Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Sat, 4 Jul 2026 18:11:17 -0400 Subject: [PATCH 05/12] docs: mark fault 12 hw-confirmed (fired at 14% battery, corpus recount) --- roborock/data/b01_q10/b01_q10_code_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index abc02236..da8111a4 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -313,7 +313,7 @@ class YXFault(RoborockModeEnum): MAIN_WHEELS_STUCK = "main_wheels_stuck", 7 ROBOT_TRAPPED = "robot_trapped", 8 # hw-confirmed CHECK_DUSTBIN_FILTER = "check_dustbin_filter", 9 - LOW_BATTERY = "low_battery", 12 + LOW_BATTERY = "low_battery", 12 # hw-confirmed (fired at 14% mid clean) TEMPERATURE_THRESHOLD = "temperature_threshold", 14 ROBOT_TILTED = "robot_tilted", 16 LIDAR_COVER_OBSTRUCTED = "lidar_cover_obstructed", 21 From 90bf0b2df35013ab41cc34248604caaba369901e Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Sun, 5 Jul 2026 16:05:18 -0400 Subject: [PATCH 06/12] docs: mark fault 407 hw-confirmed (deterministically reproduced: due schedule ignored mid-clean) --- roborock/data/b01_q10/b01_q10_code_mappings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index da8111a4..fa2c4264 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -330,6 +330,7 @@ class YXFault(RoborockModeEnum): SENSOR_NEEDS_CLEANING = "sensor_needs_cleaning", 304 # hw-confirmed (fired during auto-empty) DUST_BAG_FULL = "dust_bag_full", 310 # inferred from auto-empty context; not hw-confirmed STARTING_SCHEDULED_CLEAN = "starting_scheduled_clean", 400 # hw-confirmed x3; lifecycle, not an error + # hw-confirmed (a due scheduled clean fired mid-clean and was ignored); lifecycle, not an error CLEANING_IN_PROGRESS = "cleaning_in_progress", 407 EMPTY_DUSTBIN = "empty_dustbin", 500 # ss07 != Q7 B01Fault (lidar_blocked); app-sourced # hw-confirmed, fires per completed task; ss07 != Q7 (robot_suspended) From f5b780363191267108ed809e2e50c09e3f65b2cc Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Sun, 5 Jul 2026 16:10:33 -0400 Subject: [PATCH 07/12] docs: mark fault 46 hw-confirmed (docked with dust bag removed) --- roborock/data/b01_q10/b01_q10_code_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index fa2c4264..0b9b99eb 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -321,7 +321,7 @@ class YXFault(RoborockModeEnum): MOPPING_MODULE_STUCK = "mopping_module_stuck", 27 CARPET_AVOIDANCE = "carpet_avoidance", 28 CANNOT_CROSS_CARPET = "cannot_cross_carpet", 29 - INSTALL_DUST_BAG = "install_dust_bag", 46 + INSTALL_DUST_BAG = "install_dust_bag", 46 # hw-confirmed (docked with bag removed; fires at dock contact) MOP_MOUNT_FELL_OFF = "mop_mount_fell_off", 54 LIDAR_DIRTY = "lidar_dirty", 58 FILTER_SERVICE_LIFE = "filter_service_life", 301 From 2cd4f607cc98f696d56cb3817556c2a4596b3895 Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Sun, 5 Jul 2026 16:47:29 -0400 Subject: [PATCH 08/12] feat: re-add fault 569 as too_frequent_emptying, hw-confirmed (dock refuses a too-soon empty) --- roborock/data/b01_q10/b01_q10_code_mappings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index 0b9b99eb..dc3667cf 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -338,6 +338,8 @@ class YXFault(RoborockModeEnum): LOW_BATTERY_RESUME = "low_battery_resume", 502 # hw-confirmed; lifecycle DOCKING_ERROR = "docking_error", 503 # hw-confirmed; ss07 != Q7 (dustbin_not_installed) POSITIONING_FAILED = "positioning_failed", 556 # hw-confirmed; relocalization + # hw-confirmed (3rd auto-empty in ~15 min: dock refuses to run the cycle); ss07 != Q7 (main_wheels_entangled) + TOO_FREQUENT_EMPTYING = "too_frequent_emptying", 569 CANNOT_REACH_TARGET = "cannot_reach_target", 570 # hw-confirmed; ss07 != Q7 (main_brush_entangled) OFFLINE_WARNING_ASLEEP = "offline_warning_asleep", 588 OFFLINE_WARNING_LOW_BATTERY = "offline_warning_low_battery", 589 From fb23edea9b8a4edba4a13e40731f35e90956da89 Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Sun, 5 Jul 2026 16:56:59 -0400 Subject: [PATCH 09/12] docs: add 569 back to the docstring's Q7-divergence list --- roborock/data/b01_q10/b01_q10_code_mappings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index dc3667cf..16e42db8 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -297,8 +297,8 @@ class YXFault(RoborockModeEnum): 502 = recharge). A non-zero fault is not necessarily a blocking error. These labels follow the ss07 app text, which differs from the Q7 ``B01Fault`` - for several shared numbers (500, 501, 503, 570) -- so this is a Q10-specific - map, not a reuse of ``B01Fault``. Codes marked "hw-confirmed" were observed + for several shared numbers (500, 501, 503, 569, 570) -- so this is a + Q10-specific map, not a reuse of ``B01Fault``. Codes marked "hw-confirmed" were observed firing live on a physical ss07 in a context matching the label; the rest are app-sourced only (single device, single app version). """ From e2d050f99e6f58a7171930fc136b314609b569cf Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Sun, 5 Jul 2026 18:03:36 -0400 Subject: [PATCH 10/12] docs: mark fault 2 hw-confirmed (bumper held during commanded motion) --- roborock/data/b01_q10/b01_q10_code_mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index 16e42db8..0eff0ac4 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -306,7 +306,7 @@ class YXFault(RoborockModeEnum): UNKNOWN = "unknown", -1 NONE = "none", 0 LIDAR_BLOCKED = "lidar_blocked", 1 - BUMPER_STUCK = "bumper_stuck", 2 + BUMPER_STUCK = "bumper_stuck", 2 # hw-confirmed (bumper held in during commanded motion) ROBOT_SUSPENDED = "robot_suspended", 3 # hw-confirmed (wheels lifted off the floor mid-task) CLIFF_SENSOR_ERROR = "cliff_sensor_error", 4 # hw-confirmed (one side lifted mid-task) MAIN_BRUSH_STUCK = "main_brush_stuck", 5 # hw-confirmed (two physical brush jams) From e2829d6c61784f58179da8d1e633d627ffc19bcf Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Sun, 5 Jul 2026 18:27:46 -0400 Subject: [PATCH 11/12] docs: note the error state can carry no fault code (observed live) --- roborock/data/b01_q10/b01_q10_code_mappings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index 0eff0ac4..190a580f 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -295,6 +295,10 @@ class YXFault(RoborockModeEnum): ``dpFault`` is *overloaded*: several values are lifecycle/status rather than errors (e.g. 400 = scheduled clean starting, 501 = returning to dock, 502 = recharge). A non-zero fault is not necessarily a blocking error. + The converse also holds: the device can sit in its error state (``dpStatus`` + 12) with ``dpFault`` still 0 (observed live with the dust-bin module + removed), so a ``None``/``NONE`` fault does not imply the absence of an + error condition. These labels follow the ss07 app text, which differs from the Q7 ``B01Fault`` for several shared numbers (500, 501, 503, 569, 570) -- so this is a From ca145593c64f12912ea52580c96214b73c222b18 Mon Sep 17 00:00:00 2001 From: Andrew Yeats Date: Sun, 5 Jul 2026 18:41:18 -0400 Subject: [PATCH 12/12] docs: refer to the ss07 fault spec, not the app (review feedback) --- roborock/data/b01_q10/b01_q10_code_mappings.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/roborock/data/b01_q10/b01_q10_code_mappings.py b/roborock/data/b01_q10/b01_q10_code_mappings.py index 190a580f..203df3f2 100644 --- a/roborock/data/b01_q10/b01_q10_code_mappings.py +++ b/roborock/data/b01_q10/b01_q10_code_mappings.py @@ -290,7 +290,7 @@ class YXCarpetCleanType(RoborockModeEnum): class YXFault(RoborockModeEnum): - """Q10 (B01/ss07) ``dpFault`` (90) codes, seeded from the ss07 app's fault i18n. + """Q10 (B01/ss07) ``dpFault`` (90) codes, from the ss07 fault spec. ``dpFault`` is *overloaded*: several values are lifecycle/status rather than errors (e.g. 400 = scheduled clean starting, 501 = returning to dock, @@ -300,11 +300,11 @@ class YXFault(RoborockModeEnum): removed), so a ``None``/``NONE`` fault does not imply the absence of an error condition. - These labels follow the ss07 app text, which differs from the Q7 ``B01Fault`` - for several shared numbers (500, 501, 503, 569, 570) -- so this is a - Q10-specific map, not a reuse of ``B01Fault``. Codes marked "hw-confirmed" were observed - firing live on a physical ss07 in a context matching the label; the rest are - app-sourced only (single device, single app version). + These labels differ from the Q7 ``B01Fault`` for several shared numbers + (500, 501, 503, 569, 570) -- so this is a Q10-specific map, not a reuse of + ``B01Fault``. Codes marked "hw-confirmed" were observed firing live on a + physical ss07 in a context matching the label; the rest are from the spec + only, not yet observed live (single device). """ UNKNOWN = "unknown", -1 @@ -336,7 +336,7 @@ class YXFault(RoborockModeEnum): STARTING_SCHEDULED_CLEAN = "starting_scheduled_clean", 400 # hw-confirmed x3; lifecycle, not an error # hw-confirmed (a due scheduled clean fired mid-clean and was ignored); lifecycle, not an error CLEANING_IN_PROGRESS = "cleaning_in_progress", 407 - EMPTY_DUSTBIN = "empty_dustbin", 500 # ss07 != Q7 B01Fault (lidar_blocked); app-sourced + EMPTY_DUSTBIN = "empty_dustbin", 500 # ss07 != Q7 B01Fault (lidar_blocked); spec-only # hw-confirmed, fires per completed task; ss07 != Q7 (robot_suspended) CLEANING_COMPLETED_RETURNING = "cleaning_completed_returning", 501 LOW_BATTERY_RESUME = "low_battery_resume", 502 # hw-confirmed; lifecycle