Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions roborock/data/b01_q10/b01_q10_code_mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,70 @@ class YXCarpetCleanType(RoborockModeEnum):
CROSS = "cross", 3


class YXFault(RoborockModeEnum):
"""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,
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 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
NONE = "none", 0
LIDAR_BLOCKED = "lidar_blocked", 1
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)
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 # hw-confirmed (fired at 14% mid clean)
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 # 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
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 # 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); 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
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
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
Expand Down
8 changes: 7 additions & 1 deletion roborock/data/b01_q10/b01_q10_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
YXDeviceDustCollectionFrequency,
YXDeviceState,
YXFanLevel,
YXFault,
YXStartMethod,
YXWaterLevel,
)
Expand Down Expand Up @@ -165,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})
Expand Down Expand Up @@ -203,6 +204,11 @@ 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) -> str | None:
"""Returns the name of the current fault."""
return self.fault.value if self.fault is not None else None


@dataclass
class SoundVolume(RoborockBase):
Expand Down
39 changes: 38 additions & 1 deletion tests/data/b01_q10/test_b01_q10_code_mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -33,3 +33,40 @@ 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_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
3 changes: 2 additions & 1 deletion tests/devices/traits/b01/q10/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
YXDeviceDustCollectionFrequency,
YXDeviceState,
YXFanLevel,
YXFault,
YXWaterLevel,
)
from roborock.data.b01_q10.b01_q10_containers import dpNetInfo, dpNotDisturbExpand, dpTimeZone
Expand Down Expand Up @@ -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

Expand Down
Loading