Skip to content

Commit bad5564

Browse files
andrewlyeatsAndrew Yeats
authored andcommitted
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.
1 parent b00d60b commit bad5564

3 files changed

Lines changed: 100 additions & 1 deletion

File tree

roborock/data/b01_q10/b01_q10_code_mappings.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,64 @@ class YXCarpetCleanType(RoborockModeEnum):
289289
CROSS = "cross", 3
290290

291291

292+
class YXFault(RoborockModeEnum):
293+
"""Q10 (B01/ss07) ``dpFault`` (90) codes, seeded from the ss07 app's fault i18n.
294+
295+
``dpFault`` is *overloaded*: several values are lifecycle/status rather than
296+
errors (e.g. 400 = scheduled clean starting, 501 = returning to dock,
297+
502 = recharge). A non-zero fault is only a *blocking* error when the app
298+
routes the code into its ``D_ErrorCode`` bucket; alert/offline codes are softer.
299+
300+
These labels follow the ss07 app text, which differs from the Q7 ``B01Fault``
301+
for several shared numbers (500, 501, 503, 569, 570) -- so this is a
302+
Q10-specific map, not a reuse of ``B01Fault``. Codes marked "seen live" were
303+
observed as raw ``dpFault`` values on a physical ss07; the rest are
304+
app-sourced only (n=1, not hardware-confirmed).
305+
"""
306+
307+
UNKNOWN = "unknown", -1
308+
NONE = "none", 0
309+
LIDAR_BLOCKED = "lidar_blocked", 1
310+
BUMPER_STUCK = "bumper_stuck", 2
311+
ROBOT_SUSPENDED = "robot_suspended", 3
312+
CLIFF_SENSOR_ERROR = "cliff_sensor_error", 4
313+
MAIN_BRUSH_STUCK = "main_brush_stuck", 5
314+
MAIN_WHEELS_STUCK = "main_wheels_stuck", 7
315+
ROBOT_TRAPPED = "robot_trapped", 8 # seen live
316+
CHECK_DUSTBIN_FILTER = "check_dustbin_filter", 9
317+
LOW_BATTERY = "low_battery", 12
318+
TEMPERATURE_THRESHOLD = "temperature_threshold", 14
319+
ROBOT_TILTED = "robot_tilted", 16
320+
LIDAR_COVER_OBSTRUCTED = "lidar_cover_obstructed", 21
321+
NO_GO_ZONE_DETECTED = "no_go_zone_detected", 24
322+
MOPPING_MODULE_STUCK = "mopping_module_stuck", 27
323+
CARPET_AVOIDANCE = "carpet_avoidance", 28
324+
CANNOT_CROSS_CARPET = "cannot_cross_carpet", 29
325+
INSTALL_DUST_BAG = "install_dust_bag", 46
326+
MOP_MOUNT_FELL_OFF = "mop_mount_fell_off", 54
327+
LIDAR_DIRTY = "lidar_dirty", 58
328+
FILTER_SERVICE_LIFE = "filter_service_life", 301
329+
MAIN_BRUSH_SERVICE_LIFE = "main_brush_service_life", 302
330+
SIDE_BRUSH_SERVICE_LIFE = "side_brush_service_life", 303
331+
SENSOR_NEEDS_CLEANING = "sensor_needs_cleaning", 304
332+
DUST_BAG_FULL = "dust_bag_full", 310
333+
STARTING_SCHEDULED_CLEAN = "starting_scheduled_clean", 400 # seen live; lifecycle, not an error
334+
CLEANING_IN_PROGRESS = "cleaning_in_progress", 407
335+
EMPTY_DUSTBIN = "empty_dustbin", 500 # ss07 != Q7 B01Fault (lidar_blocked)
336+
CLEANING_COMPLETED_RETURNING = "cleaning_completed_returning", 501 # ss07 != Q7 (robot_suspended)
337+
LOW_BATTERY_RESUME = "low_battery_resume", 502
338+
DOCKING_ERROR = "docking_error", 503 # seen live; ss07 != Q7 (dustbin_not_installed)
339+
POSITIONING_FAILED = "positioning_failed", 556 # seen live; relocalization
340+
TOO_FREQUENT_EMPTYING = "too_frequent_emptying", 569 # ss07 != Q7 (main_wheels_entangled)
341+
CANNOT_REACH_TARGET = "cannot_reach_target", 570 # seen live; ss07 != Q7 (main_brush_entangled)
342+
OFFLINE_WARNING_ASLEEP = "offline_warning_asleep", 588
343+
OFFLINE_WARNING_LOW_BATTERY = "offline_warning_low_battery", 589
344+
DND_AUTO_TOPUP_DISABLED = "dnd_auto_topup_disabled", 591
345+
CLEAN_CARPET_ULTRASONIC_SENSORS = "clean_carpet_ultrasonic_sensors", 707
346+
ROBOT_ERROR_RESET = "robot_error_reset", 1002
347+
VOICE_PACK_UPDATE_AVAILABLE = "voice_pack_update_available", 3001
348+
349+
292350
class RemoteCommand(IntEnum):
293351
FORWARD = 0
294352
LEFT = 2

roborock/data/b01_q10/b01_q10_containers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
YXDeviceDustCollectionFrequency,
2424
YXDeviceState,
2525
YXFanLevel,
26+
YXFault,
2627
YXStartMethod,
2728
YXWaterLevel,
2829
)
@@ -203,6 +204,19 @@ class Q10Status(RoborockBase):
203204
filter_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.FILTER_LIFE})
204205
sensor_life: int | None = field(default=None, metadata={"dps": B01_Q10_DP.SENSOR_LIFE})
205206

207+
@property
208+
def fault_name(self) -> YXFault | None:
209+
"""Decoded ``dpFault`` label, or ``None`` for an unmapped code.
210+
211+
Additive: ``fault`` keeps the raw integer so unknown/new codes are never
212+
lost; this is the decoded companion. ``dpFault`` is overloaded (see
213+
:class:`~roborock.data.b01_q10.YXFault`), so a non-``NONE`` value is not
214+
necessarily an error.
215+
"""
216+
if self.fault is None:
217+
return None
218+
return YXFault.from_code_optional(self.fault)
219+
206220

207221
@dataclass
208222
class SoundVolume(RoborockBase):

tests/data/b01_q10/test_b01_q10_code_mappings.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from roborock.data.b01_q10 import YXDeviceState
5+
from roborock.data.b01_q10 import Q10Status, YXDeviceState, YXFault
66

77

88
@pytest.mark.parametrize(
@@ -33,3 +33,30 @@ def test_q10_status_values_are_canonical(state: YXDeviceState, string: str) -> N
3333
def test_q10_status_codes_map_to_canonical_values(code: int, expected_state: YXDeviceState) -> None:
3434
"""Code-based mapping should return canonical status values."""
3535
assert YXDeviceState.from_code(code) is expected_state
36+
37+
38+
@pytest.mark.parametrize(
39+
"code, expected",
40+
[
41+
(0, YXFault.NONE),
42+
(503, YXFault.DOCKING_ERROR), # ss07 docking error, not the Q7 "dustbin_not_installed"
43+
(570, YXFault.CANNOT_REACH_TARGET), # ss07 cannot-reach, not the Q7 "main_brush_entangled"
44+
(556, YXFault.POSITIONING_FAILED),
45+
],
46+
)
47+
def test_q10_fault_codes_map_to_ss07_labels(code: int, expected: YXFault) -> None:
48+
"""dpFault codes should decode to the ss07-correct labels."""
49+
assert YXFault.from_code(code) is expected
50+
51+
52+
def test_q10_fault_name_is_additive_and_preserves_raw_int() -> None:
53+
"""``fault_name`` decodes the label without ever losing the raw ``fault`` int."""
54+
status = Q10Status(fault=570)
55+
assert status.fault_name is YXFault.CANNOT_REACH_TARGET
56+
assert status.fault == 570
57+
58+
unmapped = Q10Status(fault=99999)
59+
assert unmapped.fault_name is None # unknown code -> None, no crash
60+
assert unmapped.fault == 99999
61+
62+
assert Q10Status(fault=None).fault_name is None

0 commit comments

Comments
 (0)