From 6ee9ac5a870c5a782755230af23572353bbfb06c Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Fri, 30 Oct 2026 21:48:15 -0400 Subject: [PATCH 1/3] feat(mower): add typed status models and protocol enums Add protobuf-named status enums with integer wire codes, explicit unknown fallbacks, and a typed GPS coordinate model. Keep compact and detailed task state domains separate, using the captured detailed domain for scalar DPS 123. Retain RoborockMowerStateCode with all existing names and values and preserve MowerStatus.from_dps. Cover typed parsing, serialization, unknown values, and the retained entry points in the mirrored mower data tests. --- roborock/data/mower/mower_code_mappings.py | 462 ++++++++++++++++++- roborock/data/mower/mower_containers.py | 117 +++-- tests/data/mower/test_mower_code_mappings.py | 148 ++++++ tests/data/mower/test_mower_containers.py | 115 +++++ tests/data/test_mower_containers.py | 119 ----- 5 files changed, 812 insertions(+), 149 deletions(-) create mode 100644 tests/data/mower/test_mower_code_mappings.py create mode 100644 tests/data/mower/test_mower_containers.py delete mode 100644 tests/data/test_mower_containers.py diff --git a/roborock/data/mower/mower_code_mappings.py b/roborock/data/mower/mower_code_mappings.py index 33a194aeb..17b5610a0 100644 --- a/roborock/data/mower/mower_code_mappings.py +++ b/roborock/data/mower/mower_code_mappings.py @@ -1,10 +1,53 @@ -"""Code mappings for Roborock mower devices.""" +"""Code mappings for Roborock mower devices. -from roborock.data.code_mappings import RoborockEnum +Status data points carry integer values, while structured ``remote_pb`` JSON +replies use protobuf enum names. The compact mowing state and detailed robot +task state are separate numeric domains and must not be interchanged. + +The new enums are :class:`RoborockModeEnum` classes whose string value is the +protobuf enum name and whose ``code`` is the integer wire value. The existing +integer-valued :class:`RoborockMowerStateCode` remains available for callers +using its original names and values. + +Enums whose protocol domain has no unknown value additionally define a +synthetic ``UNKNOWN = ("UNKNOWN", -1)`` member. It is never sent on the wire; +:meth:`MowerEnum.from_code` resolves unrecognized firmware values to it so +consumers always see an explicit unknown rather than a stale or missing value. +""" + +from typing import Self, cast + +from roborock.data.code_mappings import RoborockEnum, RoborockModeEnum + +__all__ = [ + "MowerAfsStatus", + "MowerChargeState", + "MowerChargeType", + "MowerConfMode", + "MowerDetailState", + "MowerDockState", + "MowerEffMode", + "MowerEnum", + "MowerErrorCode", + "MowerFcState", + "MowerMappingState", + "MowerMappingType", + "MowerMowState", + "MowerMowType", + "MowerNetworkChannel", + "MowerOffDockNoTaskStatus", + "MowerOfflineStatus", + "MowerOtaState", + "MowerPattern", + "MowerPendType", + "MowerRemoteState", + "MowerStartType", + "RoborockMowerStateCode", +] class RoborockMowerStateCode(RoborockEnum): - """Detailed mower operating state reported by DPS 123.""" + """Legacy detailed state codes, retained with their original names and values.""" unknown = -999 idle = 0 @@ -70,3 +113,416 @@ class RoborockMowerStateCode(RoborockEnum): charge_completed = 152 charge_waiting = 153 charge_fault = 154 + + +class MowerEnum(RoborockModeEnum): + """Base for mower enums parsed from the JSON route. + + Wire data carries the protobuf member name (``"DAILY"``), occasionally the + integer code, or the code as a decimal string. ``from_code`` accepts all + three so model fields parse through the standard conversion path, and it + maps unrecognized values to the enum's unknown member when it has one (the + member whose name starts with ``UNKNOWN`` and whose code is zero or + negative), following the Q7 ``WorkStatusMapping`` precedent. + """ + + @classmethod + def from_code(cls, code: int | str) -> Self: + """Resolve a wire value by name, string value, or integer code.""" + if (member := cls.from_any_optional(code)) is not None: + return member + for candidate in cls: + if candidate.code <= 0 and candidate.name.startswith("UNKNOWN"): + return cast(Self, candidate) + raise ValueError(f"{code!r} is not a valid value for {cls.__name__}") + + +class MowerMowType(MowerEnum): + """Mowing task selection reported by DPS 122 (``MowTypeDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + NONE = ("NONE", 0) + GLOBAL = ("GLOBAL", 1) + EDGE = ("EDGE", 2) + SELECTION = ("SELECTION", 3) + ZONING = ("ZONING", 4) + REMOTE = ("REMOTE", 5) + RANDOM = ("RANDOM", 6) + + +class MowerMowState(MowerEnum): + """Compact mowing state (``MowStateDpValue.Type``). + + This is the documented compact domain for DPS 123. Live captures report the + detailed :class:`MowerDetailState` on that data point instead, which is why + ``MowerStatus.mow_state`` uses the detailed enum; this enum remains for the + structured ``dp_points`` payloads that carry ``mow_state.type``. + """ + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + IDLE = ("IDLE", 0) + ZIGZAG_MOWING = ("ZIGZAG_MOWING", 1) + EDGE_MOWING = ("EDGE_MOWING", 2) + TRANSITION = ("TRANSITION", 3) + SUSPENDED = ("SUSPENDED", 4) + REMOTE_WALKING = ("REMOTE_WALKING", 5) + REMOTE_MOWING = ("REMOTE_MOWING", 6) + + +class MowerMappingType(MowerEnum): + """Mapping operation reported by DPS 124 (``MappingTypeDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + NONE = ("NONE", 0) + LAWN_MAPPING = ("LAWN_MAPPING", 1) + FORBIDDEN_ZONE_MAPPING = ("FORBIDDEN_ZONE_MAPPING", 2) + CHANNEL_MAPPING = ("CHANNEL_MAPPING", 3) + SPLIT_WORKSPACE = ("SPLIT_WORKSPACE", 4) + MERGE_WORKSPACE = ("MERGE_WORKSPACE", 5) + BOUNDARY_MODIFY = ("BOUNDARY_MODIFY", 6) + + +class MowerMappingState(MowerEnum): + """Mapping activity reported by DPS 125 (``MappingStateDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + IDLE = ("IDLE", 0) + AUTO_MAPPING = ("AUTO_MAPPING", 1) + TRACK_ERASE = ("TRACK_ERASE", 2) + SUSPENDED = ("SUSPENDED", 3) + SAVING_MAP = ("SAVING_MAP", 4) + REMOTE_WALKING = ("REMOTE_WALKING", 5) + REMOTE_MAPPING = ("REMOTE_MAPPING", 6) + + +class MowerOtaState(MowerEnum): + """Firmware update state reported by DPS 126 (``OtaStateDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + IDLE = ("IDLE", 0) + DOWNLOADING = ("DOWNLOADING", 1) + INSTALLING = ("INSTALLING", 2) + REBOOTING = ("REBOOTING", 3) + + +class MowerChargeState(MowerEnum): + """Charging state reported by DPS 127 (``ChargeStateDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + NOT_CHARGING = ("NOT_CHARGING", 0) + CHARGING = ("CHARGING", 1) + CHARGE_COMPLETED = ("CHARGE_COMPLETED", 2) + WAITING_CHARGE = ("WAITING_CHARGE", 3) + LOW_POWER = ("LOW_POWER", 4) + CHARGE_ERROR = ("CHARGE_ERROR", 5) + + +class MowerDockState(MowerEnum): + """Return-to-dock state reported by DPS 128 (``DockStateDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + IDLE = ("IDLE", 0) + MOVING_TO_TARGET = ("MOVING_TO_TARGET", 1) + DOCKING = ("DOCKING", 2) + SUSPENDED = ("SUSPENDED", 3) + + +class MowerChargeType(MowerEnum): + """Why the mower returned to charge, DPS 129 (``ChargeTypeDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", 0) + MANUAL_DOCK = ("MANUAL_DOCK", 1) + RAIN_DOCK = ("RAIN_DOCK", 2) + DO_NOT_DISTURB_DOCK = ("DO_NOT_DISTURB_DOCK", 3) + STANDBY_TIMEOUT_DOCK = ("STANDBY_TIMEOUT_DOCK", 4) + LOW_BATTERY_DOCK = ("LOW_BATTERY_DOCK", 5) + TASK_FINISHED_DOCK = ("TASK_FINISHED_DOCK", 6) + MOW_FAULT_DOCK = ("MOW_FAULT_DOCK", 7) + + +class MowerPendType(MowerEnum): + """Pause or interruption reason, DPS 130 (``PendTypeDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + NOT_SUSPENDED = ("NOT_SUSPENDED", 0) + APP_PAUSE = ("APP_PAUSE", 1) + BUTTON_EMERGENCY_STOP = ("BUTTON_EMERGENCY_STOP", 2) + FAULT = ("FAULT", 3) + BREAKPOINT_PAUSE = ("BREAKPOINT_PAUSE", 4) + OTHER = ("OTHER", 5) + + +class MowerRemoteState(MowerEnum): + """Remote control state, DPS 131 (``RemoteStateDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + IDLE = ("IDLE", 0) + VIDEO_REMOTE = ("VIDEO_REMOTE", 1) + REMOTE_WALKING = ("REMOTE_WALKING", 2) + + +class MowerStartType(MowerEnum): + """How the current task was started, DPS 132 (``MowStartTypeDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + DEFAULT = ("DEFAULT", 0) + APP_START = ("APP_START", 1) + BUTTON_START = ("BUTTON_START", 2) + SCHEDULE_START = ("SCHEDULE_START", 3) + + +class MowerEffMode(MowerEnum): + """Reported efficiency mode, DPS 133 (``MowEffModeDpValue.Type``). + + This is not the same enumeration as the ``effective`` preference setting. + """ + + UNKNOWN = ("UNKNOWN", 0) + DAILY = ("DAILY", 1) + PRECISE = ("PRECISE", 2) + FAST = ("FAST", 3) + + +class MowerPattern(MowerEnum): + """Reported mowing pattern, DPS 136 (``MowPattenDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", 0) + ZIGZAG = ("ZIGZAG", 1) + WELL = ("WELL", 2) + DIAMOND = ("DIAMOND", 3) + TRIANGLE = ("TRIANGLE", 4) + AUTO_DEFLECT = ("AUTO_DEFLECT", 5) + RANDOM = ("RANDOM", 6) + + +class MowerConfMode(MowerEnum): + """Preference configuration scope, DPS 137 (``MowConfModeDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", 0) + GLOBAL_CONFIG = ("GLOBAL_CONFIG", 1) + ZONE_CUSTOM = ("ZONE_CUSTOM", 2) + + +class MowerOfflineStatus(MowerEnum): + """Shutdown or reboot reason, DPS 138 (``OfflineStatusDpValue.Type``). + + This is not a reliable live connectivity flag by itself. + """ + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + ONLINE = ("ONLINE", 0) + MANUAL_SHUTDOWN = ("MANUAL_SHUTDOWN", 1) + SLEEP_SHUTDOWN = ("SLEEP_SHUTDOWN", 2) + LOW_BATTERY_SHUTDOWN = ("LOW_BATTERY_SHUTDOWN", 3) + UPGRADE_REBOOT = ("UPGRADE_REBOOT", 4) + + +class MowerFcState(MowerEnum): + """Factory or test mode indicator, DPS 141 (``FcStateDpValue.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + NOT_FC = ("NOT_FC", 0) + FC = ("FC", 1) + + +class MowerOffDockNoTaskStatus(MowerEnum): + """Mower away from the dock without an active task, DPS 143.""" + + UNKNOWN = ("UNKNOWN", 0) + WAITING = ("WAITING", 1) + SUSPENDING = ("SUSPENDING", 2) + DOCKING = ("DOCKING", 3) + REMOTING = ("REMOTING", 4) + + +class MowerAfsStatus(MowerEnum): + """Service status, DPS 144 (``AfsStatus.Type``).""" + + UNKNOWN = ("UNKNOWN", -1) + """Library-side fallback for unrecognized wire values; never sent.""" + IDLE = ("IDLE", 0) + BIT = ("BIT", 1) + MMT_OBA = ("MMT_OBA", 2) + CALIBRATION = ("CALIBRATION", 3) + ACTIVE = ("ACTIVE", 4) + INACTIVE = ("INACTIVE", 5) + + +class MowerNetworkChannel(MowerEnum): + """Current network route, DPS 145 (``NetworkChannel.Type``).""" + + UNKNOWN = ("UNKNOWN", 0) + WLAN0 = ("WLAN0", 1) + WLAN1 = ("WLAN1", 2) + MOBILE_4G = ("MOBILE_4G", 3) + + +class MowerDetailState(MowerEnum): + """Detailed task state (``rock.fsm.RobotDetailState.Type``). + + Reported in ``robot_task.robot_detail_state`` of a ``GET_ROBOT_INFO`` reply + and, on captured devices, on DPS 123. The numeric domain is separate from + the compact :class:`MowerMowState`. + """ + + UNKNOWN = ("UNKNOWN", 0) + + MAP_INITIALIZING = ("MAP_INITIALIZING", 1) + MAP_UNDOCKING = ("MAP_UNDOCKING", 2) + MAP_UNDOCK_FAULT = ("MAP_UNDOCK_FAULT", 3) + MAP_LOCATING = ("MAP_LOCATING", 4) + MAP_PREPARE_BOUNDARY = ("MAP_PREPARE_BOUNDARY", 5) + MAP_PREPARE_ISLAND = ("MAP_PREPARE_ISLAND", 6) + MAP_PREPARE_PATH = ("MAP_PREPARE_PATH", 7) + MAP_BOUNDARY = ("MAP_BOUNDARY", 8) + MAP_ISLAND = ("MAP_ISLAND", 9) + MAP_PATH = ("MAP_PATH", 10) + MAP_BOUNDARY_AUTO = ("MAP_BOUNDARY_AUTO", 11) + # The protocol spelling "MAP_ERASEING" is intentional. + MAP_ERASING = ("MAP_ERASEING", 12) + MAP_SAVE = ("MAP_SAVE", 13) + MAP_WAIT = ("MAP_WAIT", 14) + MAP_RECOVERABLE_FAULT = ("MAP_RECOVERABLE_FAULT", 15) + MAP_FAULT = ("MAP_FAULT", 16) + MAP_EMERGENCY_STOP = ("MAP_EMERGENCY_STOP", 17) + MAP_WAITING_FAULT = ("MAP_WAITING_FAULT", 18) + MAP_NO_AVOID = ("MAP_NO_AVOID", 19) + MAP_PREPARE_NO_AVOID = ("MAP_PREPARE_NO_AVOID", 20) + MAP_RESET_PASSAGE = ("MAP_RESET_PASSAGE", 21) + MAP_PREPARE_RESET_PASSAGE = ("MAP_PREPARE_RESET_PASSAGE", 22) + MAP_EDIT_BOUNDARY = ("MAP_EDIT_BOUNDARY", 23) + MAP_PREPARE_EDIT_BOUNDARY = ("MAP_PREPARE_EDIT_BOUNDARY", 24) + MAP_CONFIRM_EDIT_BOUNDARY = ("MAP_CONFIRM_EDIT_BOUNDARY", 25) + + MOW_INITIALIZING = ("MOW_INITIALIZING", 51) + MOW_UNDOCKING = ("MOW_UNDOCKING", 52) + MOW_LOCATING = ("MOW_LOCATING", 53) + MOW_ADJUST_CUTTER = ("MOW_ADJUST_CUTTER", 54) + MOW_ZIG_ZAG = ("MOW_ZIG_ZAG", 55) + MOW_EDGE = ("MOW_EDGE", 56) + MOW_GOTO = ("MOW_GOTO", 57) + MOW_SUSPEND = ("MOW_SUSPEND", 58) + MOW_RECOVERABLE_FAULT = ("MOW_RECOVERABLE_FAULT", 59) + MOW_FAULT = ("MOW_FAULT", 60) + MOW_DOCK_RAINFALL = ("MOW_DOCK_RAINFALL", 61) + MOW_DOCK_NOTDISTURB = ("MOW_DOCK_NOTDISTURB", 62) + MOW_DOCK_LOW_BATTERY = ("MOW_DOCK_LOW_BATTERY", 63) + MOW_WAIT = ("MOW_WAIT", 64) + MOW_PREPARE_REMOTE = ("MOW_PREPARE_REMOTE", 65) + MOW_REMOTE = ("MOW_REMOTE", 66) + MOW_EMERGENCY_STOP = ("MOW_EMERGENCY_STOP", 67) + MOW_DOCK_MANUAL = ("MOW_DOCK_MANUAL", 68) + MOW_DOCK_FAULT = ("MOW_DOCK_FAULT", 69) + MOW_REMOTE_UNDOCKING = ("MOW_REMOTE_UNDOCKING", 70) + MOW_TO_DOCK_INITIALIZING = ("MOW_TO_DOCK_INITIALIZING", 71) + MOW_TO_DOCK_LOCATING = ("MOW_TO_DOCK_LOCATING", 72) + MOW_TO_DOCK_RECOVERABLE_FAULT = ("MOW_TO_DOCK_RECOVERABLE_FAULT", 73) + MOW_TO_DOCK_FAULT = ("MOW_TO_DOCK_FAULT", 74) + MOW_TO_DOCK_EMERGENCY_STOP = ("MOW_TO_DOCK_EMERGENCY_STOP", 75) + MOW_TO_DOCK_CHARGING = ("MOW_TO_DOCK_CHARGING", 76) + MOW_TO_DOCK_CHARGE_COMPLETED = ("MOW_TO_DOCK_CHARGE_COMPLETED", 77) + MOW_PAUSE = ("MOW_PAUSE", 78) + + FREE = ("FREE", 101) + FREE_INITIALIZING = ("FREE_INITIALIZING", 102) + FREE_LOCATING = ("FREE_LOCATING", 103) + FREE_DOCK_MANUAL = ("FREE_DOCK_MANUAL", 104) + FREE_DOCK_MOW_END = ("FREE_DOCK_MOW_END", 105) + FREE_DOCK_PLAN_END = ("FREE_DOCK_PLAN_END", 106) + FREE_EMERGENCY_STOP = ("FREE_EMERGENCY_STOP", 107) + FREE_RECOVERABLE_FAULT = ("FREE_RECOVERABLE_FAULT", 108) + FREE_FAULT = ("FREE_FAULT", 109) + FREE_BATTERY_HOLD_MODE = ("FREE_BATTERY_HOLD_MODE", 110) + + CHARGING = ("CHARGING", 151) + CHARGE_COMPLETED = ("CHARGE_COMPLETED", 152) + CHARGE_WAITING = ("CHARGE_WAITING", 153) + CHARGE_FAULT = ("CHARGE_FAULT", 154) + + +class MowerErrorCode(MowerEnum): + """User-facing error code reported by DPS 120. + + ``UNKNOWN`` (0) is also the value reported when no error is active, so it + must not be surfaced as a fault by itself. + """ + + UNKNOWN = ("UNKNOWN", 0) + MAIN_CUTTER_OVER_CURRENT = ("MAIN_CUTTER_OVER_CURRENT", 1) + MAIN_CUTTER_OVER_TEMPERATURE = ("MAIN_CUTTER_OVER_TEMPERATURE", 2) + MAIN_CUTTER_DYNAMIC_UNBALANCE = ("MAIN_CUTTER_DYNAMIC_UNBALANCE", 3) + EDGE_CUTTER_OVER_CURRENT = ("EDGE_CUTTER_OVER_CURRENT", 4) + EDGE_CUTTER_OVER_TEMPERATURE = ("EDGE_CUTTER_OVER_TEMPERATURE", 5) + EDGE_CUTTER_DYNAMIC_UNBALANCE = ("EDGE_CUTTER_DYNAMIC_UNBALANCE", 6) + HEIGHT_MODULE_ADJUSTING_INCOMPLETE = ("HEIGHT_MODULE_ADJUSTING_INCOMPLETE", 7) + TRAVEL_MOTOR_LEFT_FRONT_OVER_CURRENT = ("TRAVEL_MOTOR_LEFT_FRONT_OVER_CURRENT", 8) + TRAVEL_MOTOR_RIGHT_FRONT_OVER_CURRENT = ("TRAVEL_MOTOR_RIGHT_FRONT_OVER_CURRENT", 9) + TRAVEL_MOTOR_LEFT_REAR_OVER_CURRENT = ("TRAVEL_MOTOR_LEFT_REAR_OVER_CURRENT", 10) + TRAVEL_MOTOR_RIGHT_REAR_OVER_CURRENT = ("TRAVEL_MOTOR_RIGHT_REAR_OVER_CURRENT", 11) + TRAVEL_MOTOR_OVER_TEMPERATURE = ("TRAVEL_MOTOR_OVER_TEMPERATURE", 12) + STEERING_MOTOR_LEFT_FRONT_OVER_CURRENT = ("STEERING_MOTOR_LEFT_FRONT_OVER_CURRENT", 13) + STEERING_MOTOR_RIGHT_FRONT_OVER_CURRENT = ("STEERING_MOTOR_RIGHT_FRONT_OVER_CURRENT", 14) + STEERING_MOTOR_OVER_TEMPERATURE = ("STEERING_MOTOR_OVER_TEMPERATURE", 15) + STOP = ("STOP", 16) + TILT = ("TILT", 17) + LIFT = ("LIFT", 18) + FLIP = ("FLIP", 19) + OUT_OF_BOUND = ("OUT_OF_BOUND", 20) + BUMPER = ("BUMPER", 21) + BATTERY_DISCHARGE_OVER_TEMPERATURE = ("BATTERY_DISCHARGE_OVER_TEMPERATURE", 22) + BATTERY_DISCHARGE_LOW_TEMPERATURE = ("BATTERY_DISCHARGE_LOW_TEMPERATURE", 23) + BATTERY_UNOFFICIAL = ("BATTERY_UNOFFICIAL", 24) + DIRTY_CHARGING_CLIPS = ("DIRTY_CHARGING_CLIPS", 25) + CHARGE_STATION_NO_POWER = ("CHARGE_STATION_NO_POWER", 26) + CAMERA_DIRTY_OR_OBSTRUCTED = ("CAMERA_DIRTY_OR_OBSTRUCTED", 27) + CAMERA_OVER_TEMPERATURE = ("CAMERA_OVER_TEMPERATURE", 28) + CAMERA_LED_OBSTRUCTED = ("CAMERA_LED_OBSTRUCTED", 29) + LIDAR_DIRTY = ("LIDAR_DIRTY", 30) + LIDAR_OVER_TEMPERATURE = ("LIDAR_OVER_TEMPERATURE", 31) + LOCATION_FAILED = ("LOCATION_FAILED", 32) + LOCATION_AT_ISLAND = ("LOCATION_AT_ISLAND", 33) + LOCATION_AT_NON_COMPLIANT_SLOPE = ("LOCATION_AT_NON_COMPLIANT_SLOPE", 34) + BE_TRAPPED = ("BE_TRAPPED", 35) + CHARGE_STATION_OR_RTK_SLIGHT_MOVEMENT_DOCK = ("CHARGE_STATION_OR_RTK_SLIGHT_MOVEMENT_DOCK", 36) + N_RTK_SIGNIFICANT_MOVEMENT = ("N_RTK_SIGNIFICANT_MOVEMENT", 37) + RTK_SIGNIFICANT_MOVEMENT = ("RTK_SIGNIFICANT_MOVEMENT", 38) + DOCK_CHARGE_STATION_SEARCH_FAIL = ("DOCK_CHARGE_STATION_SEARCH_FAIL", 39) + DOCK_QRCODE_SCAN_FAIL = ("DOCK_QRCODE_SCAN_FAIL", 40) + DOCK_ADJUST_ERROR = ("DOCK_ADJUST_ERROR", 41) + UNDOCK_ERROR = ("UNDOCK_ERROR", 42) + BATTERY_CHARGE_OVER_TEMPERATURE = ("BATTERY_CHARGE_OVER_TEMPERATURE", 43) + BATTERY_CHARGE_LOW_TEMPERATURE = ("BATTERY_CHARGE_LOW_TEMPERATURE", 44) + CAMERA_SEVERELY_DIRTY = ("CAMERA_SEVERELY_DIRTY", 45) + NO_PLUGIN_KEY = ("NO_PLUGIN_KEY", 46) + CAMERA_REAR_SIDE_DIRTY = ("CAMERA_REAR_SIDE_DIRTY", 48) + N_RTK_NETWORK_ERROR = ("N_RTK_NETWORK_ERROR", 55) + MAIN_CUTTER_FAULT = ("MAIN_CUTTER_FAULT", 101) + EDGE_CUTTER_FAULT = ("EDGE_CUTTER_FAULT", 102) + HEIGHT_MODULE_FAULT = ("HEIGHT_MODULE_FAULT", 103) + BATTERY_FAULT = ("BATTERY_FAULT", 104) + CAMERA_FAULT = ("CAMERA_FAULT", 105) + CAMERA_LED_FAULT = ("CAMERA_LED_FAULT", 106) + MAIN_BOARD_FAULT = ("MAIN_BOARD_FAULT", 107) + LIDAR_FAULT = ("LIDAR_FAULT", 108) + TRAVEL_MOTOR_LEFT_FRONT_FAULT = ("TRAVEL_MOTOR_LEFT_FRONT_FAULT", 109) + TRAVEL_MOTOR_RIGHT_FRONT_FAULT = ("TRAVEL_MOTOR_RIGHT_FRONT_FAULT", 110) + TRAVEL_MOTOR_LEFT_REAR_FAULT = ("TRAVEL_MOTOR_LEFT_REAR_FAULT", 111) + TRAVEL_MOTOR_RIGHT_REAR_FAULT = ("TRAVEL_MOTOR_RIGHT_REAR_FAULT", 112) + STEERING_MOTOR_LEFT_FRONT_FAULT = ("STEERING_MOTOR_LEFT_FRONT_FAULT", 113) + STEERING_MOTOR_RIGHT_FRONT_FAULT = ("STEERING_MOTOR_RIGHT_FRONT_FAULT", 114) + LED_SCREEN_FAULT = ("LED_SCREEN_FAULT", 115) + LEFT_SIDE_CAMERA_FAULT = ("LEFT_SIDE_CAMERA_FAULT", 116) + RIGHT_SIDE_CAMERA_FAULT = ("RIGHT_SIDE_CAMERA_FAULT", 117) + REAR_SIDE_CAMERA_FAULT = ("REAR_SIDE_CAMERA_FAULT", 118) diff --git a/roborock/data/mower/mower_containers.py b/roborock/data/mower/mower_containers.py index d7d4b3822..ab06ccd71 100644 --- a/roborock/data/mower/mower_containers.py +++ b/roborock/data/mower/mower_containers.py @@ -6,52 +6,115 @@ from roborock.data.containers import RoborockBase from roborock.roborock_message import RoborockMowerDataProtocol -from .mower_code_mappings import RoborockMowerStateCode +from .mower_code_mappings import ( + MowerAfsStatus, + MowerChargeState, + MowerChargeType, + MowerConfMode, + MowerDetailState, + MowerDockState, + MowerEffMode, + MowerErrorCode, + MowerFcState, + MowerMappingState, + MowerMappingType, + MowerMowType, + MowerNetworkChannel, + MowerOffDockNoTaskStatus, + MowerOfflineStatus, + MowerOtaState, + MowerPattern, + MowerPendType, + MowerRemoteState, + MowerStartType, +) + +__all__ = [ + "MowerGpsCoordinate", + "MowerStatus", +] + + +@dataclass +class MowerGpsCoordinate(RoborockBase): + """Mower GPS position (``GpsCoordinateDpValue``).""" + + latitude: float | None = None + longitude: float | None = None @dataclass class MowerStatus(RoborockBase): - """Core mower status backed by mower DPS updates.""" + """Core mower status backed by mower DPS updates (data points 120-145). + + Absent values mean the device has not reported that data point; for example + a missing battery is unknown, not zero. + """ - error_code: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.ERROR_CODE}) + error_code: MowerErrorCode | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.ERROR_CODE}) battery: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.BATTERY}) - mow_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_TYPE}) - mow_state: RoborockMowerStateCode | None = field( - default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_STATE} + """Battery percentage.""" + mow_type: MowerMowType | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_TYPE}) + mow_state: MowerDetailState | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_STATE}) + """Task state on data point 123. + + The protocol declares this data point as the compact ``MowStateDpValue`` + domain, but captured devices report the detailed ``RobotDetailState`` + domain here (for example ``58`` = ``MOW_SUSPEND``), so the detailed enum is + used. + """ + mapping_type: MowerMappingType | None = field( + default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_TYPE} ) - mapping_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_TYPE}) - mapping_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_STATE}) - ota_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.OTA_STATE}) - charge_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.CHARGE_STATE}) - dock_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.DOCK_STATE}) - charge_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.CHARGE_TYPE}) - pend_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.PEND_TYPE}) - remote_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.REMOTE_STATE}) - mow_start_type: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_START_TYPE}) - mow_eff_mode: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_EFF_MODE}) + mapping_state: MowerMappingState | None = field( + default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_STATE} + ) + ota_state: MowerOtaState | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.OTA_STATE}) + charge_state: MowerChargeState | None = field( + default=None, metadata={"dps": RoborockMowerDataProtocol.CHARGE_STATE} + ) + dock_state: MowerDockState | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.DOCK_STATE}) + charge_type: MowerChargeType | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.CHARGE_TYPE}) + pend_type: MowerPendType | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.PEND_TYPE}) + remote_state: MowerRemoteState | None = field( + default=None, metadata={"dps": RoborockMowerDataProtocol.REMOTE_STATE} + ) + mow_start_type: MowerStartType | None = field( + default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_START_TYPE} + ) + mow_eff_mode: MowerEffMode | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_EFF_MODE}) mow_height: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_HEIGHT}) + """Reported cutting height in device units.""" mow_direction_angle: int | None = field( default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_DIRECTION_ANGLE} ) - mow_pattern: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PATTERN}) - mow_conf_mode: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_CONF_MODE}) - offline_status: Any | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.OFFLINE_STATUS}) + """Reported mowing direction in degrees.""" + mow_pattern: MowerPattern | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PATTERN}) + mow_conf_mode: MowerConfMode | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_CONF_MODE}) + offline_status: MowerOfflineStatus | None = field( + default=None, metadata={"dps": RoborockMowerDataProtocol.OFFLINE_STATUS} + ) mow_progress: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PROGRESS}) + """Compact task progress in a model-specific scale.""" blade_lifespan: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.BLADE_LIFESPAN}) - fc_state: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.FC_STATE}) - gps_coordinate: Any | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.GPS_COORDINATE}) - off_dock_no_task_status: int | None = field( + """Blade usage or lifespan in reported units.""" + fc_state: MowerFcState | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.FC_STATE}) + gps_coordinate: MowerGpsCoordinate | None = field( + default=None, metadata={"dps": RoborockMowerDataProtocol.GPS_COORDINATE} + ) + off_dock_no_task_status: MowerOffDockNoTaskStatus | None = field( default=None, metadata={"dps": RoborockMowerDataProtocol.OFF_DOCK_NO_TASK_STATUS} ) - afs_status: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.AFS_STATUS}) - network_channel: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.NETWORK_CHANNEL}) + afs_status: MowerAfsStatus | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.AFS_STATUS}) + network_channel: MowerNetworkChannel | None = field( + default=None, metadata={"dps": RoborockMowerDataProtocol.NETWORK_CHANNEL} + ) @classmethod def from_dps(cls, dps: dict[int | str, Any] | None) -> Self: - """Create mower status from raw DPS data. + """Create typed status from integer or string DPS keys. - Cloud snapshots use string DPS keys while decoded MQTT messages may use - integer keys. Unknown and malformed keys are ignored. + Unknown and malformed keys are ignored, matching the existing factory. """ field_by_dps = { int(field_info.metadata["dps"]): field_info.name diff --git a/tests/data/mower/test_mower_code_mappings.py b/tests/data/mower/test_mower_code_mappings.py new file mode 100644 index 000000000..ff87a6c39 --- /dev/null +++ b/tests/data/mower/test_mower_code_mappings.py @@ -0,0 +1,148 @@ +"""Tests for mower code mappings.""" + +import pytest + +from roborock.data import mower +from roborock.data.code_mappings import RoborockModeEnum +from roborock.data.mower import ( + MowerChargeState, + MowerDetailState, + MowerEffMode, + MowerEnum, + MowerErrorCode, + MowerFcState, + MowerMowState, + MowerMowType, + MowerNetworkChannel, + MowerOfflineStatus, + MowerPendType, + RoborockMowerStateCode, +) + +ALL_MOWER_ENUMS: list[type[RoborockModeEnum]] = sorted( + ( + obj + for obj in vars(mower).values() + if isinstance(obj, type) and issubclass(obj, MowerEnum) and obj is not MowerEnum + ), + key=lambda cls: cls.__name__, +) + + +def test_all_mower_enums_are_exported() -> None: + """Every mower enum is exported from its module.""" + assert ALL_MOWER_ENUMS + assert all(cls.__name__ in mower.mower_code_mappings.__all__ for cls in ALL_MOWER_ENUMS) + + +@pytest.mark.parametrize("enum_cls", ALL_MOWER_ENUMS, ids=lambda cls: cls.__name__) +def test_mower_enum_values_are_protobuf_names(enum_cls: type[RoborockModeEnum]) -> None: + """The string value is the protobuf enum name, so it can round-trip through the JSON route.""" + for member in enum_cls: + assert enum_cls.from_value(member.value) is member + assert enum_cls.from_code(member.code) is member + + +@pytest.mark.parametrize("enum_cls", ALL_MOWER_ENUMS, ids=lambda cls: cls.__name__) +def test_mower_enum_codes_are_unique(enum_cls: type[RoborockModeEnum]) -> None: + """Each wire code maps to exactly one member.""" + codes = [member.code for member in enum_cls] + assert len(codes) == len(set(codes)) + + +@pytest.mark.parametrize( + ("enum_cls", "code", "expected"), + [ + # Values from the live RockNeo Q105 status capture. + (MowerMowType, 1, MowerMowType.GLOBAL), + (MowerDetailState, 58, MowerDetailState.MOW_SUSPEND), + (MowerEffMode, 1, MowerEffMode.DAILY), + (MowerNetworkChannel, 1, MowerNetworkChannel.WLAN0), + # Zero has a named meaning in every compact enum. + (MowerMowState, 0, MowerMowState.IDLE), + (MowerChargeState, 0, MowerChargeState.NOT_CHARGING), + (MowerPendType, 0, MowerPendType.NOT_SUSPENDED), + (MowerOfflineStatus, 0, MowerOfflineStatus.ONLINE), + (MowerFcState, 0, MowerFcState.NOT_FC), + # Boundaries of the detailed state domain. + (MowerDetailState, 25, MowerDetailState.MAP_CONFIRM_EDIT_BOUNDARY), + (MowerDetailState, 78, MowerDetailState.MOW_PAUSE), + (MowerDetailState, 110, MowerDetailState.FREE_BATTERY_HOLD_MODE), + (MowerDetailState, 154, MowerDetailState.CHARGE_FAULT), + # Error codes have gaps in their numbering. + (MowerErrorCode, 48, MowerErrorCode.CAMERA_REAR_SIDE_DIRTY), + (MowerErrorCode, 55, MowerErrorCode.N_RTK_NETWORK_ERROR), + (MowerErrorCode, 118, MowerErrorCode.REAR_SIDE_CAMERA_FAULT), + ], +) +def test_mower_enum_code_lookup(enum_cls: type[RoborockModeEnum], code: int, expected: RoborockModeEnum) -> None: + assert enum_cls.from_code(code) is expected + + +@pytest.mark.parametrize( + ("enum_cls", "code"), + [ + # Gaps inside the detailed-state and error-code domains. + (MowerDetailState, 26), + (MowerDetailState, 50), + (MowerDetailState, 79), + (MowerErrorCode, 47), + (MowerErrorCode, 56), + # The detailed value seen on DPS 123 is not part of the compact domain. + (MowerMowState, 58), + ], +) +def test_mower_enum_unknown_codes_resolve_to_none(enum_cls: type[RoborockModeEnum], code: int) -> None: + assert enum_cls.from_code_optional(code) is None + + +@pytest.mark.parametrize( + ("member", "wire_name"), + [ + (MowerDetailState.MAP_ERASING, "MAP_ERASEING"), + ], +) +def test_misspelled_wire_names_are_preserved(member: RoborockModeEnum, wire_name: str) -> None: + """Protocol misspellings are kept as the value so the JSON route still matches.""" + assert member.value == wire_name + assert type(member).from_value(wire_name) is member + + +@pytest.mark.parametrize( + ("enum_cls", "raw", "expected"), + [ + (MowerEffMode, "DAILY", MowerEffMode.DAILY), + (MowerEffMode, 1, MowerEffMode.DAILY), + (MowerEffMode, "1", MowerEffMode.DAILY), + (MowerDetailState, "MAP_ERASEING", MowerDetailState.MAP_ERASING), + # Unknown values resolve to the explicit unknown member, synthetic or protocol-defined. + (MowerMowType, 9999, MowerMowType.UNKNOWN), + (MowerMowType, "FUTURE_TYPE", MowerMowType.UNKNOWN), + (MowerDetailState, 9999, MowerDetailState.UNKNOWN), + ], +) +def test_from_code_accepts_wire_forms_and_falls_back( + enum_cls: type[MowerEnum], raw: int | str, expected: MowerEnum +) -> None: + assert enum_cls.from_code(raw) is expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + (0, RoborockMowerStateCode.idle), + (12, RoborockMowerStateCode.map_erasing), + (58, RoborockMowerStateCode.mow_suspend), + (61, RoborockMowerStateCode.mow_docked_rainfall), + (151, RoborockMowerStateCode.charge_charging), + (154, RoborockMowerStateCode.charge_fault), + (9999, RoborockMowerStateCode.unknown), + ], +) +def test_legacy_state_enum_remains_available(code: int, expected: RoborockMowerStateCode) -> None: + """Existing imports, lower-case member names and integer lookups still work.""" + from roborock.data import RoborockMowerStateCode as exported_enum + + assert exported_enum is RoborockMowerStateCode + assert exported_enum(code) is expected + assert exported_enum.unknown.value == -999 diff --git a/tests/data/mower/test_mower_containers.py b/tests/data/mower/test_mower_containers.py new file mode 100644 index 000000000..13221bad6 --- /dev/null +++ b/tests/data/mower/test_mower_containers.py @@ -0,0 +1,115 @@ +"""Tests for mower data containers.""" + +import dataclasses + +import pytest + +from roborock.data import ( + MowerDetailState, + MowerEffMode, + MowerErrorCode, + MowerGpsCoordinate, + MowerMowType, + MowerNetworkChannel, + MowerStatus, +) +from roborock.roborock_message import RoborockMowerDataProtocol + + +def test_mower_status_from_captured_values() -> None: + """The values from the captured snapshot parse to their typed fields.""" + status = MowerStatus.from_dict( + {"battery": 78, "mow_type": 1, "mow_state": 58, "mow_eff_mode": 1, "mow_progress": 55, "network_channel": 1} + ) + + assert status.battery == 78 + assert status.mow_type is MowerMowType.GLOBAL + assert status.mow_state is MowerDetailState.MOW_SUSPEND + assert status.mow_eff_mode is MowerEffMode.DAILY + assert status.mow_progress == 55 + assert status.network_channel is MowerNetworkChannel.WLAN0 + # Fields absent from a report are unknown, not zero. + assert status.error_code is None + assert status.mow_height is None + + +def test_mower_status_from_dict_accepts_camel_case_and_nested_gps() -> None: + status = MowerStatus.from_dict( + { + "errorCode": 18, + "battery": 82, + "mowState": 55, + "mowHeight": 70, + "gpsCoordinate": {"latitude": 1.5, "longitude": -2.25}, + "unknown": "ignored", + } + ) + + assert status.error_code is MowerErrorCode.LIFT + assert status.battery == 82 + assert status.mow_state is MowerDetailState.MOW_ZIG_ZAG + assert status.mow_height == 70 + assert status.gps_coordinate == MowerGpsCoordinate(latitude=1.5, longitude=-2.25) + + +def test_mower_status_round_trips_through_as_dict() -> None: + status = MowerStatus.from_dict({"battery": 78, "mow_state": 58, "mow_type": 1}) + + serialized = status.as_dict() + + assert serialized["mowState"] == "MOW_SUSPEND" + assert serialized["mowType"] == "GLOBAL" + assert MowerStatus.from_dict(serialized) == status + + +def test_mower_status_resolves_unknown_firmware_codes_to_unknown() -> None: + """An unmapped code resolves to the explicit UNKNOWN member instead of raising.""" + status = MowerStatus.from_dict({"mow_state": 9999, "battery": 50, "mow_type": 9999}) + + assert status.mow_state is MowerDetailState.UNKNOWN + assert status.mow_type is MowerMowType.UNKNOWN + assert status.battery == 50 + + +def test_mower_status_dps_metadata_matches_protocol() -> None: + dps_by_field = { + field.name: field.metadata["dps"] for field in dataclasses.fields(MowerStatus) if "dps" in field.metadata + } + status_protocols = {protocol for protocol in RoborockMowerDataProtocol if 120 <= protocol.value <= 145} + + assert set(dps_by_field.values()) == status_protocols + assert len(dps_by_field) == len(status_protocols) + + +def test_mower_status_from_dps() -> None: + """The existing factory accepts mixed keys and returns typed status fields.""" + status = MowerStatus.from_dps( + { + "121": 78, + 122: 1, + "123": 58, + "133": 1, + "142": {"latitude": 1.5, "longitude": -2.25}, + "145": 1, + "battery": 0, + "123.0": 55, + "999": "ignored", + } + ) + + assert status.battery == 78 + assert status.mow_type is MowerMowType.GLOBAL + assert status.mow_state is MowerDetailState.MOW_SUSPEND + assert status.mow_eff_mode is MowerEffMode.DAILY + assert status.gps_coordinate == MowerGpsCoordinate(latitude=1.5, longitude=-2.25) + assert status.network_channel is MowerNetworkChannel.WLAN0 + assert status.error_code is None + + +@pytest.mark.parametrize("dps", [None, {}]) +def test_mower_status_from_empty_dps(dps: dict[int | str, object] | None) -> None: + assert MowerStatus.from_dps(dps) == MowerStatus() + + +def test_mower_status_from_dps_unknown_state() -> None: + assert MowerStatus.from_dps({"123": 9999}).mow_state is MowerDetailState.UNKNOWN diff --git a/tests/data/test_mower_containers.py b/tests/data/test_mower_containers.py deleted file mode 100644 index 423d4ce04..000000000 --- a/tests/data/test_mower_containers.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Test mower data containers.""" - -import dataclasses - -from roborock.data import MowerStatus, RoborockMowerStateCode -from roborock.roborock_message import RoborockMowerDataProtocol - - -def test_mower_status_from_dict() -> None: - status = MowerStatus.from_dict( - { - "error_code": 0, - "battery": 82, - "mow_state": 3, - "mowHeight": 70, - "gpsCoordinate": {"latitude": 1, "longitude": 2}, - "unknown": "ignored", - } - ) - - assert status.error_code == 0 - assert status.battery == 82 - assert status.mow_state is RoborockMowerStateCode.map_undock_fault - assert status.mow_height == 70 - assert status.gps_coordinate == {"latitude": 1, "longitude": 2} - - -def test_mower_status_from_dps() -> None: - status = MowerStatus.from_dps( - { - "121": 78, - 122: 1, - "123": 58, - "133": 1, - "139": 55, - "142": "", - "145": 1, - "unknown": "ignored", - "999": "ignored", - } - ) - - assert status.battery == 78 - assert status.mow_type == 1 - assert status.mow_state is RoborockMowerStateCode.mow_suspend - assert status.mow_eff_mode == 1 - assert status.mow_progress == 55 - assert status.gps_coordinate == "" - assert status.network_channel == 1 - assert status.error_code is None - assert status.mow_height is None - - -def test_mower_status_from_empty_dps() -> None: - assert MowerStatus.from_dps(None) == MowerStatus() - - -def test_mower_status_unknown_state_uses_unknown() -> None: - status = MowerStatus.from_dps({"123": 9999}) - - assert status.mow_state is RoborockMowerStateCode.unknown - - -def test_mower_state_code_mappings() -> None: - assert RoborockMowerStateCode(0) is RoborockMowerStateCode.idle - assert RoborockMowerStateCode(55) is RoborockMowerStateCode.mow_zig_zag - assert RoborockMowerStateCode(56) is RoborockMowerStateCode.mow_edge - assert RoborockMowerStateCode(71) is RoborockMowerStateCode.mow_to_dock_initializing - assert RoborockMowerStateCode(151) is RoborockMowerStateCode.charge_charging - assert RoborockMowerStateCode(154) is RoborockMowerStateCode.charge_fault - - -def test_mower_status_dps_metadata() -> None: - dps_by_field = { - field.name: field.metadata["dps"] - for field in dataclasses.fields(MowerStatus) - if field.metadata and "dps" in field.metadata - } - - assert dps_by_field == { - "error_code": RoborockMowerDataProtocol.ERROR_CODE, - "battery": RoborockMowerDataProtocol.BATTERY, - "mow_type": RoborockMowerDataProtocol.MOW_TYPE, - "mow_state": RoborockMowerDataProtocol.MOW_STATE, - "mapping_type": RoborockMowerDataProtocol.MAPPING_TYPE, - "mapping_state": RoborockMowerDataProtocol.MAPPING_STATE, - "ota_state": RoborockMowerDataProtocol.OTA_STATE, - "charge_state": RoborockMowerDataProtocol.CHARGE_STATE, - "dock_state": RoborockMowerDataProtocol.DOCK_STATE, - "charge_type": RoborockMowerDataProtocol.CHARGE_TYPE, - "pend_type": RoborockMowerDataProtocol.PEND_TYPE, - "remote_state": RoborockMowerDataProtocol.REMOTE_STATE, - "mow_start_type": RoborockMowerDataProtocol.MOW_START_TYPE, - "mow_eff_mode": RoborockMowerDataProtocol.MOW_EFF_MODE, - "mow_height": RoborockMowerDataProtocol.MOW_HEIGHT, - "mow_direction_angle": RoborockMowerDataProtocol.MOW_DIRECTION_ANGLE, - "mow_pattern": RoborockMowerDataProtocol.MOW_PATTERN, - "mow_conf_mode": RoborockMowerDataProtocol.MOW_CONF_MODE, - "offline_status": RoborockMowerDataProtocol.OFFLINE_STATUS, - "mow_progress": RoborockMowerDataProtocol.MOW_PROGRESS, - "blade_lifespan": RoborockMowerDataProtocol.BLADE_LIFESPAN, - "fc_state": RoborockMowerDataProtocol.FC_STATE, - "gps_coordinate": RoborockMowerDataProtocol.GPS_COORDINATE, - "off_dock_no_task_status": RoborockMowerDataProtocol.OFF_DOCK_NO_TASK_STATUS, - "afs_status": RoborockMowerDataProtocol.AFS_STATUS, - "network_channel": RoborockMowerDataProtocol.NETWORK_CHANNEL, - } - - -def test_mower_command_dps_are_not_status_fields() -> None: - status_dps = { - field.metadata["dps"] for field in dataclasses.fields(MowerStatus) if field.metadata and "dps" in field.metadata - } - - assert RoborockMowerDataProtocol.START not in status_dps - assert RoborockMowerDataProtocol.DOCK not in status_dps - assert RoborockMowerDataProtocol.PAUSE not in status_dps - assert RoborockMowerDataProtocol.RESUME not in status_dps - assert RoborockMowerDataProtocol.STOP not in status_dps From cf5ea13c424e888a58e2308e4cbdfa4784311c29 Mon Sep 17 00:00:00 2001 From: Luke Date: Fri, 30 Oct 2026 21:53:54 -0400 Subject: [PATCH 2/3] fix(mower): return cls.UNKNOWN directly in MowerEnum.from_code --- roborock/data/mower/mower_code_mappings.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/roborock/data/mower/mower_code_mappings.py b/roborock/data/mower/mower_code_mappings.py index 17b5610a0..6b74b354e 100644 --- a/roborock/data/mower/mower_code_mappings.py +++ b/roborock/data/mower/mower_code_mappings.py @@ -131,10 +131,7 @@ def from_code(cls, code: int | str) -> Self: """Resolve a wire value by name, string value, or integer code.""" if (member := cls.from_any_optional(code)) is not None: return member - for candidate in cls: - if candidate.code <= 0 and candidate.name.startswith("UNKNOWN"): - return cast(Self, candidate) - raise ValueError(f"{code!r} is not a valid value for {cls.__name__}") + return cast(Self, cls.UNKNOWN) # type: ignore[attr-defined] class MowerMowType(MowerEnum): From 47fc9b1ae4fe0bc61b7b0ea505d1ea341db4226a Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 31 Oct 2026 21:13:36 -0400 Subject: [PATCH 3/3] fix(mower): lowercase enum values, trim docstrings, and remove legacy state enum --- roborock/data/mower/mower_code_mappings.py | 681 ++++++++----------- roborock/data/mower/mower_containers.py | 25 +- tests/data/mower/test_mower_code_mappings.py | 33 +- tests/data/mower/test_mower_containers.py | 4 +- 4 files changed, 289 insertions(+), 454 deletions(-) diff --git a/roborock/data/mower/mower_code_mappings.py b/roborock/data/mower/mower_code_mappings.py index 6b74b354e..3dc907b6c 100644 --- a/roborock/data/mower/mower_code_mappings.py +++ b/roborock/data/mower/mower_code_mappings.py @@ -1,23 +1,8 @@ -"""Code mappings for Roborock mower devices. - -Status data points carry integer values, while structured ``remote_pb`` JSON -replies use protobuf enum names. The compact mowing state and detailed robot -task state are separate numeric domains and must not be interchanged. - -The new enums are :class:`RoborockModeEnum` classes whose string value is the -protobuf enum name and whose ``code`` is the integer wire value. The existing -integer-valued :class:`RoborockMowerStateCode` remains available for callers -using its original names and values. - -Enums whose protocol domain has no unknown value additionally define a -synthetic ``UNKNOWN = ("UNKNOWN", -1)`` member. It is never sent on the wire; -:meth:`MowerEnum.from_code` resolves unrecognized firmware values to it so -consumers always see an explicit unknown rather than a stale or missing value. -""" +"""Code mappings for Roborock mower devices.""" from typing import Self, cast -from roborock.data.code_mappings import RoborockEnum, RoborockModeEnum +from roborock.data.code_mappings import RoborockModeEnum __all__ = [ "MowerAfsStatus", @@ -42,89 +27,11 @@ "MowerPendType", "MowerRemoteState", "MowerStartType", - "RoborockMowerStateCode", ] -class RoborockMowerStateCode(RoborockEnum): - """Legacy detailed state codes, retained with their original names and values.""" - - unknown = -999 - idle = 0 - - map_initializing = 1 - map_undocking = 2 - map_undock_fault = 3 - map_locating = 4 - map_prepare_boundary = 5 - map_prepare_island = 6 - map_prepare_path = 7 - map_boundary = 8 - map_island = 9 - map_path = 10 - map_boundary_auto = 11 - map_erasing = 12 - map_save = 13 - map_wait = 14 - map_recoverable_fault = 15 - map_fault = 16 - map_emergency_stop = 17 - map_waiting_fault = 18 - - mow_initializing = 51 - mow_undocking = 52 - mow_locating = 53 - mow_adjust_cutter = 54 - mow_zig_zag = 55 - mow_edge = 56 - mow_goto = 57 - mow_suspend = 58 - mow_recoverable_fault = 59 - mow_fault = 60 - mow_docked_rainfall = 61 - mow_docked_do_not_disturb = 62 - mow_docked_low_battery = 63 - mow_wait = 64 - mow_prepare_remote = 65 - mow_remote = 66 - mow_emergency_stop = 67 - mow_docked_manual = 68 - mow_dock_fault = 69 - mow_remote_undocking = 70 - mow_to_dock_initializing = 71 - mow_to_dock_locating = 72 - mow_to_dock_recoverable_fault = 73 - mow_to_dock_fault = 74 - mow_to_dock_emergency_stop = 75 - mow_to_dock_charging = 76 - mow_to_dock_charge_completed = 77 - - free = 101 - free_initializing = 102 - free_locating = 103 - free_docked_manual = 104 - free_docked_mow_end = 105 - free_docked_plan_end = 106 - free_emergency_stop = 107 - free_recoverable_fault = 108 - free_fault = 109 - - charge_charging = 151 - charge_completed = 152 - charge_waiting = 153 - charge_fault = 154 - - class MowerEnum(RoborockModeEnum): - """Base for mower enums parsed from the JSON route. - - Wire data carries the protobuf member name (``"DAILY"``), occasionally the - integer code, or the code as a decimal string. ``from_code`` accepts all - three so model fields parse through the standard conversion path, and it - maps unrecognized values to the enum's unknown member when it has one (the - member whose name starts with ``UNKNOWN`` and whose code is zero or - negative), following the Q7 ``WorkStatusMapping`` precedent. - """ + """Base for mower enums.""" @classmethod def from_code(cls, code: int | str) -> Self: @@ -135,391 +42,357 @@ def from_code(cls, code: int | str) -> Self: class MowerMowType(MowerEnum): - """Mowing task selection reported by DPS 122 (``MowTypeDpValue.Type``).""" + """Mowing task selection reported by DPS 122.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - NONE = ("NONE", 0) - GLOBAL = ("GLOBAL", 1) - EDGE = ("EDGE", 2) - SELECTION = ("SELECTION", 3) - ZONING = ("ZONING", 4) - REMOTE = ("REMOTE", 5) - RANDOM = ("RANDOM", 6) + UNKNOWN = ("unknown", -1) + NONE = ("none", 0) + GLOBAL = ("global", 1) + EDGE = ("edge", 2) + SELECTION = ("selection", 3) + ZONING = ("zoning", 4) + REMOTE = ("remote", 5) + RANDOM = ("random", 6) class MowerMowState(MowerEnum): - """Compact mowing state (``MowStateDpValue.Type``). - - This is the documented compact domain for DPS 123. Live captures report the - detailed :class:`MowerDetailState` on that data point instead, which is why - ``MowerStatus.mow_state`` uses the detailed enum; this enum remains for the - structured ``dp_points`` payloads that carry ``mow_state.type``. - """ + """Compact mowing state (MowStateDpValue.Type).""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - IDLE = ("IDLE", 0) - ZIGZAG_MOWING = ("ZIGZAG_MOWING", 1) - EDGE_MOWING = ("EDGE_MOWING", 2) - TRANSITION = ("TRANSITION", 3) - SUSPENDED = ("SUSPENDED", 4) - REMOTE_WALKING = ("REMOTE_WALKING", 5) - REMOTE_MOWING = ("REMOTE_MOWING", 6) + UNKNOWN = ("unknown", -1) + IDLE = ("idle", 0) + ZIGZAG_MOWING = ("zigzag_mowing", 1) + EDGE_MOWING = ("edge_mowing", 2) + TRANSITION = ("transition", 3) + SUSPENDED = ("suspended", 4) + REMOTE_WALKING = ("remote_walking", 5) + REMOTE_MOWING = ("remote_mowing", 6) class MowerMappingType(MowerEnum): - """Mapping operation reported by DPS 124 (``MappingTypeDpValue.Type``).""" + """Mapping operation reported by DPS 124.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - NONE = ("NONE", 0) - LAWN_MAPPING = ("LAWN_MAPPING", 1) - FORBIDDEN_ZONE_MAPPING = ("FORBIDDEN_ZONE_MAPPING", 2) - CHANNEL_MAPPING = ("CHANNEL_MAPPING", 3) - SPLIT_WORKSPACE = ("SPLIT_WORKSPACE", 4) - MERGE_WORKSPACE = ("MERGE_WORKSPACE", 5) - BOUNDARY_MODIFY = ("BOUNDARY_MODIFY", 6) + UNKNOWN = ("unknown", -1) + NONE = ("none", 0) + LAWN_MAPPING = ("lawn_mapping", 1) + FORBIDDEN_ZONE_MAPPING = ("forbidden_zone_mapping", 2) + CHANNEL_MAPPING = ("channel_mapping", 3) + SPLIT_WORKSPACE = ("split_workspace", 4) + MERGE_WORKSPACE = ("merge_workspace", 5) + BOUNDARY_MODIFY = ("boundary_modify", 6) class MowerMappingState(MowerEnum): - """Mapping activity reported by DPS 125 (``MappingStateDpValue.Type``).""" + """Mapping activity reported by DPS 125.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - IDLE = ("IDLE", 0) - AUTO_MAPPING = ("AUTO_MAPPING", 1) - TRACK_ERASE = ("TRACK_ERASE", 2) - SUSPENDED = ("SUSPENDED", 3) - SAVING_MAP = ("SAVING_MAP", 4) - REMOTE_WALKING = ("REMOTE_WALKING", 5) - REMOTE_MAPPING = ("REMOTE_MAPPING", 6) + UNKNOWN = ("unknown", -1) + IDLE = ("idle", 0) + AUTO_MAPPING = ("auto_mapping", 1) + TRACK_ERASE = ("track_erase", 2) + SUSPENDED = ("suspended", 3) + SAVING_MAP = ("saving_map", 4) + REMOTE_WALKING = ("remote_walking", 5) + REMOTE_MAPPING = ("remote_mapping", 6) class MowerOtaState(MowerEnum): - """Firmware update state reported by DPS 126 (``OtaStateDpValue.Type``).""" + """Firmware update state reported by DPS 126.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - IDLE = ("IDLE", 0) - DOWNLOADING = ("DOWNLOADING", 1) - INSTALLING = ("INSTALLING", 2) - REBOOTING = ("REBOOTING", 3) + UNKNOWN = ("unknown", -1) + IDLE = ("idle", 0) + DOWNLOADING = ("downloading", 1) + INSTALLING = ("installing", 2) + REBOOTING = ("rebooting", 3) class MowerChargeState(MowerEnum): - """Charging state reported by DPS 127 (``ChargeStateDpValue.Type``).""" + """Charging state reported by DPS 127.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - NOT_CHARGING = ("NOT_CHARGING", 0) - CHARGING = ("CHARGING", 1) - CHARGE_COMPLETED = ("CHARGE_COMPLETED", 2) - WAITING_CHARGE = ("WAITING_CHARGE", 3) - LOW_POWER = ("LOW_POWER", 4) - CHARGE_ERROR = ("CHARGE_ERROR", 5) + UNKNOWN = ("unknown", -1) + NOT_CHARGING = ("not_charging", 0) + CHARGING = ("charging", 1) + CHARGE_COMPLETED = ("charge_completed", 2) + WAITING_CHARGE = ("waiting_charge", 3) + LOW_POWER = ("low_power", 4) + CHARGE_ERROR = ("charge_error", 5) class MowerDockState(MowerEnum): - """Return-to-dock state reported by DPS 128 (``DockStateDpValue.Type``).""" + """Return-to-dock state reported by DPS 128.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - IDLE = ("IDLE", 0) - MOVING_TO_TARGET = ("MOVING_TO_TARGET", 1) - DOCKING = ("DOCKING", 2) - SUSPENDED = ("SUSPENDED", 3) + UNKNOWN = ("unknown", -1) + IDLE = ("idle", 0) + MOVING_TO_TARGET = ("moving_to_target", 1) + DOCKING = ("docking", 2) + SUSPENDED = ("suspended", 3) class MowerChargeType(MowerEnum): - """Why the mower returned to charge, DPS 129 (``ChargeTypeDpValue.Type``).""" + """Reason for returning to charge reported by DPS 129.""" - UNKNOWN = ("UNKNOWN", 0) - MANUAL_DOCK = ("MANUAL_DOCK", 1) - RAIN_DOCK = ("RAIN_DOCK", 2) - DO_NOT_DISTURB_DOCK = ("DO_NOT_DISTURB_DOCK", 3) - STANDBY_TIMEOUT_DOCK = ("STANDBY_TIMEOUT_DOCK", 4) - LOW_BATTERY_DOCK = ("LOW_BATTERY_DOCK", 5) - TASK_FINISHED_DOCK = ("TASK_FINISHED_DOCK", 6) - MOW_FAULT_DOCK = ("MOW_FAULT_DOCK", 7) + UNKNOWN = ("unknown", 0) + MANUAL_DOCK = ("manual_dock", 1) + RAIN_DOCK = ("rain_dock", 2) + DO_NOT_DISTURB_DOCK = ("do_not_disturb_dock", 3) + STANDBY_TIMEOUT_DOCK = ("standby_timeout_dock", 4) + LOW_BATTERY_DOCK = ("low_battery_dock", 5) + TASK_FINISHED_DOCK = ("task_finished_dock", 6) + MOW_FAULT_DOCK = ("mow_fault_dock", 7) class MowerPendType(MowerEnum): - """Pause or interruption reason, DPS 130 (``PendTypeDpValue.Type``).""" + """Pause or interruption reason reported by DPS 130.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - NOT_SUSPENDED = ("NOT_SUSPENDED", 0) - APP_PAUSE = ("APP_PAUSE", 1) - BUTTON_EMERGENCY_STOP = ("BUTTON_EMERGENCY_STOP", 2) - FAULT = ("FAULT", 3) - BREAKPOINT_PAUSE = ("BREAKPOINT_PAUSE", 4) - OTHER = ("OTHER", 5) + UNKNOWN = ("unknown", -1) + NOT_SUSPENDED = ("not_suspended", 0) + APP_PAUSE = ("app_pause", 1) + BUTTON_EMERGENCY_STOP = ("button_emergency_stop", 2) + FAULT = ("fault", 3) + BREAKPOINT_PAUSE = ("breakpoint_pause", 4) + OTHER = ("other", 5) class MowerRemoteState(MowerEnum): - """Remote control state, DPS 131 (``RemoteStateDpValue.Type``).""" + """Remote control state reported by DPS 131.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - IDLE = ("IDLE", 0) - VIDEO_REMOTE = ("VIDEO_REMOTE", 1) - REMOTE_WALKING = ("REMOTE_WALKING", 2) + UNKNOWN = ("unknown", -1) + IDLE = ("idle", 0) + VIDEO_REMOTE = ("video_remote", 1) + REMOTE_WALKING = ("remote_walking", 2) class MowerStartType(MowerEnum): - """How the current task was started, DPS 132 (``MowStartTypeDpValue.Type``).""" + """Task start trigger reported by DPS 132.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - DEFAULT = ("DEFAULT", 0) - APP_START = ("APP_START", 1) - BUTTON_START = ("BUTTON_START", 2) - SCHEDULE_START = ("SCHEDULE_START", 3) + UNKNOWN = ("unknown", -1) + DEFAULT = ("default", 0) + APP_START = ("app_start", 1) + BUTTON_START = ("button_start", 2) + SCHEDULE_START = ("schedule_start", 3) class MowerEffMode(MowerEnum): - """Reported efficiency mode, DPS 133 (``MowEffModeDpValue.Type``). + """Reported efficiency mode on DPS 133.""" - This is not the same enumeration as the ``effective`` preference setting. - """ - - UNKNOWN = ("UNKNOWN", 0) - DAILY = ("DAILY", 1) - PRECISE = ("PRECISE", 2) - FAST = ("FAST", 3) + UNKNOWN = ("unknown", 0) + DAILY = ("daily", 1) + PRECISE = ("precise", 2) + FAST = ("fast", 3) class MowerPattern(MowerEnum): - """Reported mowing pattern, DPS 136 (``MowPattenDpValue.Type``).""" + """Reported mowing pattern on DPS 136.""" - UNKNOWN = ("UNKNOWN", 0) - ZIGZAG = ("ZIGZAG", 1) - WELL = ("WELL", 2) - DIAMOND = ("DIAMOND", 3) - TRIANGLE = ("TRIANGLE", 4) - AUTO_DEFLECT = ("AUTO_DEFLECT", 5) - RANDOM = ("RANDOM", 6) + UNKNOWN = ("unknown", 0) + ZIGZAG = ("zigzag", 1) + WELL = ("well", 2) + DIAMOND = ("diamond", 3) + TRIANGLE = ("triangle", 4) + AUTO_DEFLECT = ("auto_deflect", 5) + RANDOM = ("random", 6) class MowerConfMode(MowerEnum): - """Preference configuration scope, DPS 137 (``MowConfModeDpValue.Type``).""" + """Preference configuration scope on DPS 137.""" - UNKNOWN = ("UNKNOWN", 0) - GLOBAL_CONFIG = ("GLOBAL_CONFIG", 1) - ZONE_CUSTOM = ("ZONE_CUSTOM", 2) + UNKNOWN = ("unknown", 0) + GLOBAL_CONFIG = ("global_config", 1) + ZONE_CUSTOM = ("zone_custom", 2) class MowerOfflineStatus(MowerEnum): - """Shutdown or reboot reason, DPS 138 (``OfflineStatusDpValue.Type``). - - This is not a reliable live connectivity flag by itself. - """ + """Shutdown or reboot reason on DPS 138.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - ONLINE = ("ONLINE", 0) - MANUAL_SHUTDOWN = ("MANUAL_SHUTDOWN", 1) - SLEEP_SHUTDOWN = ("SLEEP_SHUTDOWN", 2) - LOW_BATTERY_SHUTDOWN = ("LOW_BATTERY_SHUTDOWN", 3) - UPGRADE_REBOOT = ("UPGRADE_REBOOT", 4) + UNKNOWN = ("unknown", -1) + ONLINE = ("online", 0) + MANUAL_SHUTDOWN = ("manual_shutdown", 1) + SLEEP_SHUTDOWN = ("sleep_shutdown", 2) + LOW_BATTERY_SHUTDOWN = ("low_battery_shutdown", 3) + UPGRADE_REBOOT = ("upgrade_reboot", 4) class MowerFcState(MowerEnum): - """Factory or test mode indicator, DPS 141 (``FcStateDpValue.Type``).""" + """Factory or test mode indicator on DPS 141.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - NOT_FC = ("NOT_FC", 0) - FC = ("FC", 1) + UNKNOWN = ("unknown", -1) + NOT_FC = ("not_fc", 0) + FC = ("fc", 1) class MowerOffDockNoTaskStatus(MowerEnum): - """Mower away from the dock without an active task, DPS 143.""" + """Mower off-dock status without an active task on DPS 143.""" - UNKNOWN = ("UNKNOWN", 0) - WAITING = ("WAITING", 1) - SUSPENDING = ("SUSPENDING", 2) - DOCKING = ("DOCKING", 3) - REMOTING = ("REMOTING", 4) + UNKNOWN = ("unknown", 0) + WAITING = ("waiting", 1) + SUSPENDING = ("suspending", 2) + DOCKING = ("docking", 3) + REMOTING = ("remoting", 4) class MowerAfsStatus(MowerEnum): - """Service status, DPS 144 (``AfsStatus.Type``).""" + """Service status on DPS 144.""" - UNKNOWN = ("UNKNOWN", -1) - """Library-side fallback for unrecognized wire values; never sent.""" - IDLE = ("IDLE", 0) - BIT = ("BIT", 1) - MMT_OBA = ("MMT_OBA", 2) - CALIBRATION = ("CALIBRATION", 3) - ACTIVE = ("ACTIVE", 4) - INACTIVE = ("INACTIVE", 5) + UNKNOWN = ("unknown", -1) + IDLE = ("idle", 0) + BIT = ("bit", 1) + MMT_OBA = ("mmt_oba", 2) + CALIBRATION = ("calibration", 3) + ACTIVE = ("active", 4) + INACTIVE = ("inactive", 5) class MowerNetworkChannel(MowerEnum): - """Current network route, DPS 145 (``NetworkChannel.Type``).""" + """Current network route on DPS 145.""" - UNKNOWN = ("UNKNOWN", 0) - WLAN0 = ("WLAN0", 1) - WLAN1 = ("WLAN1", 2) - MOBILE_4G = ("MOBILE_4G", 3) + UNKNOWN = ("unknown", 0) + WLAN0 = ("wlan0", 1) + WLAN1 = ("wlan1", 2) + MOBILE_4G = ("mobile_4g", 3) class MowerDetailState(MowerEnum): - """Detailed task state (``rock.fsm.RobotDetailState.Type``). - - Reported in ``robot_task.robot_detail_state`` of a ``GET_ROBOT_INFO`` reply - and, on captured devices, on DPS 123. The numeric domain is separate from - the compact :class:`MowerMowState`. - """ - - UNKNOWN = ("UNKNOWN", 0) - - MAP_INITIALIZING = ("MAP_INITIALIZING", 1) - MAP_UNDOCKING = ("MAP_UNDOCKING", 2) - MAP_UNDOCK_FAULT = ("MAP_UNDOCK_FAULT", 3) - MAP_LOCATING = ("MAP_LOCATING", 4) - MAP_PREPARE_BOUNDARY = ("MAP_PREPARE_BOUNDARY", 5) - MAP_PREPARE_ISLAND = ("MAP_PREPARE_ISLAND", 6) - MAP_PREPARE_PATH = ("MAP_PREPARE_PATH", 7) - MAP_BOUNDARY = ("MAP_BOUNDARY", 8) - MAP_ISLAND = ("MAP_ISLAND", 9) - MAP_PATH = ("MAP_PATH", 10) - MAP_BOUNDARY_AUTO = ("MAP_BOUNDARY_AUTO", 11) + """Detailed task state reported on DPS 123 and in GET_ROBOT_INFO replies.""" + + UNKNOWN = ("unknown", 0) + + MAP_INITIALIZING = ("map_initializing", 1) + MAP_UNDOCKING = ("map_undocking", 2) + MAP_UNDOCK_FAULT = ("map_undock_fault", 3) + MAP_LOCATING = ("map_locating", 4) + MAP_PREPARE_BOUNDARY = ("map_prepare_boundary", 5) + MAP_PREPARE_ISLAND = ("map_prepare_island", 6) + MAP_PREPARE_PATH = ("map_prepare_path", 7) + MAP_BOUNDARY = ("map_boundary", 8) + MAP_ISLAND = ("map_island", 9) + MAP_PATH = ("map_path", 10) + MAP_BOUNDARY_AUTO = ("map_boundary_auto", 11) # The protocol spelling "MAP_ERASEING" is intentional. - MAP_ERASING = ("MAP_ERASEING", 12) - MAP_SAVE = ("MAP_SAVE", 13) - MAP_WAIT = ("MAP_WAIT", 14) - MAP_RECOVERABLE_FAULT = ("MAP_RECOVERABLE_FAULT", 15) - MAP_FAULT = ("MAP_FAULT", 16) - MAP_EMERGENCY_STOP = ("MAP_EMERGENCY_STOP", 17) - MAP_WAITING_FAULT = ("MAP_WAITING_FAULT", 18) - MAP_NO_AVOID = ("MAP_NO_AVOID", 19) - MAP_PREPARE_NO_AVOID = ("MAP_PREPARE_NO_AVOID", 20) - MAP_RESET_PASSAGE = ("MAP_RESET_PASSAGE", 21) - MAP_PREPARE_RESET_PASSAGE = ("MAP_PREPARE_RESET_PASSAGE", 22) - MAP_EDIT_BOUNDARY = ("MAP_EDIT_BOUNDARY", 23) - MAP_PREPARE_EDIT_BOUNDARY = ("MAP_PREPARE_EDIT_BOUNDARY", 24) - MAP_CONFIRM_EDIT_BOUNDARY = ("MAP_CONFIRM_EDIT_BOUNDARY", 25) - - MOW_INITIALIZING = ("MOW_INITIALIZING", 51) - MOW_UNDOCKING = ("MOW_UNDOCKING", 52) - MOW_LOCATING = ("MOW_LOCATING", 53) - MOW_ADJUST_CUTTER = ("MOW_ADJUST_CUTTER", 54) - MOW_ZIG_ZAG = ("MOW_ZIG_ZAG", 55) - MOW_EDGE = ("MOW_EDGE", 56) - MOW_GOTO = ("MOW_GOTO", 57) - MOW_SUSPEND = ("MOW_SUSPEND", 58) - MOW_RECOVERABLE_FAULT = ("MOW_RECOVERABLE_FAULT", 59) - MOW_FAULT = ("MOW_FAULT", 60) - MOW_DOCK_RAINFALL = ("MOW_DOCK_RAINFALL", 61) - MOW_DOCK_NOTDISTURB = ("MOW_DOCK_NOTDISTURB", 62) - MOW_DOCK_LOW_BATTERY = ("MOW_DOCK_LOW_BATTERY", 63) - MOW_WAIT = ("MOW_WAIT", 64) - MOW_PREPARE_REMOTE = ("MOW_PREPARE_REMOTE", 65) - MOW_REMOTE = ("MOW_REMOTE", 66) - MOW_EMERGENCY_STOP = ("MOW_EMERGENCY_STOP", 67) - MOW_DOCK_MANUAL = ("MOW_DOCK_MANUAL", 68) - MOW_DOCK_FAULT = ("MOW_DOCK_FAULT", 69) - MOW_REMOTE_UNDOCKING = ("MOW_REMOTE_UNDOCKING", 70) - MOW_TO_DOCK_INITIALIZING = ("MOW_TO_DOCK_INITIALIZING", 71) - MOW_TO_DOCK_LOCATING = ("MOW_TO_DOCK_LOCATING", 72) - MOW_TO_DOCK_RECOVERABLE_FAULT = ("MOW_TO_DOCK_RECOVERABLE_FAULT", 73) - MOW_TO_DOCK_FAULT = ("MOW_TO_DOCK_FAULT", 74) - MOW_TO_DOCK_EMERGENCY_STOP = ("MOW_TO_DOCK_EMERGENCY_STOP", 75) - MOW_TO_DOCK_CHARGING = ("MOW_TO_DOCK_CHARGING", 76) - MOW_TO_DOCK_CHARGE_COMPLETED = ("MOW_TO_DOCK_CHARGE_COMPLETED", 77) - MOW_PAUSE = ("MOW_PAUSE", 78) - - FREE = ("FREE", 101) - FREE_INITIALIZING = ("FREE_INITIALIZING", 102) - FREE_LOCATING = ("FREE_LOCATING", 103) - FREE_DOCK_MANUAL = ("FREE_DOCK_MANUAL", 104) - FREE_DOCK_MOW_END = ("FREE_DOCK_MOW_END", 105) - FREE_DOCK_PLAN_END = ("FREE_DOCK_PLAN_END", 106) - FREE_EMERGENCY_STOP = ("FREE_EMERGENCY_STOP", 107) - FREE_RECOVERABLE_FAULT = ("FREE_RECOVERABLE_FAULT", 108) - FREE_FAULT = ("FREE_FAULT", 109) - FREE_BATTERY_HOLD_MODE = ("FREE_BATTERY_HOLD_MODE", 110) - - CHARGING = ("CHARGING", 151) - CHARGE_COMPLETED = ("CHARGE_COMPLETED", 152) - CHARGE_WAITING = ("CHARGE_WAITING", 153) - CHARGE_FAULT = ("CHARGE_FAULT", 154) + MAP_ERASING = ("map_eraseing", 12) + MAP_SAVE = ("map_save", 13) + MAP_WAIT = ("map_wait", 14) + MAP_RECOVERABLE_FAULT = ("map_recoverable_fault", 15) + MAP_FAULT = ("map_fault", 16) + MAP_EMERGENCY_STOP = ("map_emergency_stop", 17) + MAP_WAITING_FAULT = ("map_waiting_fault", 18) + MAP_NO_AVOID = ("map_no_avoid", 19) + MAP_PREPARE_NO_AVOID = ("map_prepare_no_avoid", 20) + MAP_RESET_PASSAGE = ("map_reset_passage", 21) + MAP_PREPARE_RESET_PASSAGE = ("map_prepare_reset_passage", 22) + MAP_EDIT_BOUNDARY = ("map_edit_boundary", 23) + MAP_PREPARE_EDIT_BOUNDARY = ("map_prepare_edit_boundary", 24) + MAP_CONFIRM_EDIT_BOUNDARY = ("map_confirm_edit_boundary", 25) + + MOW_INITIALIZING = ("mow_initializing", 51) + MOW_UNDOCKING = ("mow_undocking", 52) + MOW_LOCATING = ("mow_locating", 53) + MOW_ADJUST_CUTTER = ("mow_adjust_cutter", 54) + MOW_ZIG_ZAG = ("mow_zig_zag", 55) + MOW_EDGE = ("mow_edge", 56) + MOW_GOTO = ("mow_goto", 57) + MOW_SUSPEND = ("mow_suspend", 58) + MOW_RECOVERABLE_FAULT = ("mow_recoverable_fault", 59) + MOW_FAULT = ("mow_fault", 60) + MOW_DOCK_RAINFALL = ("mow_dock_rainfall", 61) + MOW_DOCK_NOTDISTURB = ("mow_dock_notdisturb", 62) + MOW_DOCK_LOW_BATTERY = ("mow_dock_low_battery", 63) + MOW_WAIT = ("mow_wait", 64) + MOW_PREPARE_REMOTE = ("mow_prepare_remote", 65) + MOW_REMOTE = ("mow_remote", 66) + MOW_EMERGENCY_STOP = ("mow_emergency_stop", 67) + MOW_DOCK_MANUAL = ("mow_dock_manual", 68) + MOW_DOCK_FAULT = ("mow_dock_fault", 69) + MOW_REMOTE_UNDOCKING = ("mow_remote_undocking", 70) + MOW_TO_DOCK_INITIALIZING = ("mow_to_dock_initializing", 71) + MOW_TO_DOCK_LOCATING = ("mow_to_dock_locating", 72) + MOW_TO_DOCK_RECOVERABLE_FAULT = ("mow_to_dock_recoverable_fault", 73) + MOW_TO_DOCK_FAULT = ("mow_to_dock_fault", 74) + MOW_TO_DOCK_EMERGENCY_STOP = ("mow_to_dock_emergency_stop", 75) + MOW_TO_DOCK_CHARGING = ("mow_to_dock_charging", 76) + MOW_TO_DOCK_CHARGE_COMPLETED = ("mow_to_dock_charge_completed", 77) + MOW_PAUSE = ("mow_pause", 78) + + FREE = ("free", 101) + FREE_INITIALIZING = ("free_initializing", 102) + FREE_LOCATING = ("free_locating", 103) + FREE_DOCK_MANUAL = ("free_dock_manual", 104) + FREE_DOCK_MOW_END = ("free_dock_mow_end", 105) + FREE_DOCK_PLAN_END = ("free_dock_plan_end", 106) + FREE_EMERGENCY_STOP = ("free_emergency_stop", 107) + FREE_RECOVERABLE_FAULT = ("free_recoverable_fault", 108) + FREE_FAULT = ("free_fault", 109) + FREE_BATTERY_HOLD_MODE = ("free_battery_hold_mode", 110) + + CHARGING = ("charging", 151) + CHARGE_COMPLETED = ("charge_completed", 152) + CHARGE_WAITING = ("charge_waiting", 153) + CHARGE_FAULT = ("charge_fault", 154) class MowerErrorCode(MowerEnum): - """User-facing error code reported by DPS 120. - - ``UNKNOWN`` (0) is also the value reported when no error is active, so it - must not be surfaced as a fault by itself. - """ - - UNKNOWN = ("UNKNOWN", 0) - MAIN_CUTTER_OVER_CURRENT = ("MAIN_CUTTER_OVER_CURRENT", 1) - MAIN_CUTTER_OVER_TEMPERATURE = ("MAIN_CUTTER_OVER_TEMPERATURE", 2) - MAIN_CUTTER_DYNAMIC_UNBALANCE = ("MAIN_CUTTER_DYNAMIC_UNBALANCE", 3) - EDGE_CUTTER_OVER_CURRENT = ("EDGE_CUTTER_OVER_CURRENT", 4) - EDGE_CUTTER_OVER_TEMPERATURE = ("EDGE_CUTTER_OVER_TEMPERATURE", 5) - EDGE_CUTTER_DYNAMIC_UNBALANCE = ("EDGE_CUTTER_DYNAMIC_UNBALANCE", 6) - HEIGHT_MODULE_ADJUSTING_INCOMPLETE = ("HEIGHT_MODULE_ADJUSTING_INCOMPLETE", 7) - TRAVEL_MOTOR_LEFT_FRONT_OVER_CURRENT = ("TRAVEL_MOTOR_LEFT_FRONT_OVER_CURRENT", 8) - TRAVEL_MOTOR_RIGHT_FRONT_OVER_CURRENT = ("TRAVEL_MOTOR_RIGHT_FRONT_OVER_CURRENT", 9) - TRAVEL_MOTOR_LEFT_REAR_OVER_CURRENT = ("TRAVEL_MOTOR_LEFT_REAR_OVER_CURRENT", 10) - TRAVEL_MOTOR_RIGHT_REAR_OVER_CURRENT = ("TRAVEL_MOTOR_RIGHT_REAR_OVER_CURRENT", 11) - TRAVEL_MOTOR_OVER_TEMPERATURE = ("TRAVEL_MOTOR_OVER_TEMPERATURE", 12) - STEERING_MOTOR_LEFT_FRONT_OVER_CURRENT = ("STEERING_MOTOR_LEFT_FRONT_OVER_CURRENT", 13) - STEERING_MOTOR_RIGHT_FRONT_OVER_CURRENT = ("STEERING_MOTOR_RIGHT_FRONT_OVER_CURRENT", 14) - STEERING_MOTOR_OVER_TEMPERATURE = ("STEERING_MOTOR_OVER_TEMPERATURE", 15) - STOP = ("STOP", 16) - TILT = ("TILT", 17) - LIFT = ("LIFT", 18) - FLIP = ("FLIP", 19) - OUT_OF_BOUND = ("OUT_OF_BOUND", 20) - BUMPER = ("BUMPER", 21) - BATTERY_DISCHARGE_OVER_TEMPERATURE = ("BATTERY_DISCHARGE_OVER_TEMPERATURE", 22) - BATTERY_DISCHARGE_LOW_TEMPERATURE = ("BATTERY_DISCHARGE_LOW_TEMPERATURE", 23) - BATTERY_UNOFFICIAL = ("BATTERY_UNOFFICIAL", 24) - DIRTY_CHARGING_CLIPS = ("DIRTY_CHARGING_CLIPS", 25) - CHARGE_STATION_NO_POWER = ("CHARGE_STATION_NO_POWER", 26) - CAMERA_DIRTY_OR_OBSTRUCTED = ("CAMERA_DIRTY_OR_OBSTRUCTED", 27) - CAMERA_OVER_TEMPERATURE = ("CAMERA_OVER_TEMPERATURE", 28) - CAMERA_LED_OBSTRUCTED = ("CAMERA_LED_OBSTRUCTED", 29) - LIDAR_DIRTY = ("LIDAR_DIRTY", 30) - LIDAR_OVER_TEMPERATURE = ("LIDAR_OVER_TEMPERATURE", 31) - LOCATION_FAILED = ("LOCATION_FAILED", 32) - LOCATION_AT_ISLAND = ("LOCATION_AT_ISLAND", 33) - LOCATION_AT_NON_COMPLIANT_SLOPE = ("LOCATION_AT_NON_COMPLIANT_SLOPE", 34) - BE_TRAPPED = ("BE_TRAPPED", 35) - CHARGE_STATION_OR_RTK_SLIGHT_MOVEMENT_DOCK = ("CHARGE_STATION_OR_RTK_SLIGHT_MOVEMENT_DOCK", 36) - N_RTK_SIGNIFICANT_MOVEMENT = ("N_RTK_SIGNIFICANT_MOVEMENT", 37) - RTK_SIGNIFICANT_MOVEMENT = ("RTK_SIGNIFICANT_MOVEMENT", 38) - DOCK_CHARGE_STATION_SEARCH_FAIL = ("DOCK_CHARGE_STATION_SEARCH_FAIL", 39) - DOCK_QRCODE_SCAN_FAIL = ("DOCK_QRCODE_SCAN_FAIL", 40) - DOCK_ADJUST_ERROR = ("DOCK_ADJUST_ERROR", 41) - UNDOCK_ERROR = ("UNDOCK_ERROR", 42) - BATTERY_CHARGE_OVER_TEMPERATURE = ("BATTERY_CHARGE_OVER_TEMPERATURE", 43) - BATTERY_CHARGE_LOW_TEMPERATURE = ("BATTERY_CHARGE_LOW_TEMPERATURE", 44) - CAMERA_SEVERELY_DIRTY = ("CAMERA_SEVERELY_DIRTY", 45) - NO_PLUGIN_KEY = ("NO_PLUGIN_KEY", 46) - CAMERA_REAR_SIDE_DIRTY = ("CAMERA_REAR_SIDE_DIRTY", 48) - N_RTK_NETWORK_ERROR = ("N_RTK_NETWORK_ERROR", 55) - MAIN_CUTTER_FAULT = ("MAIN_CUTTER_FAULT", 101) - EDGE_CUTTER_FAULT = ("EDGE_CUTTER_FAULT", 102) - HEIGHT_MODULE_FAULT = ("HEIGHT_MODULE_FAULT", 103) - BATTERY_FAULT = ("BATTERY_FAULT", 104) - CAMERA_FAULT = ("CAMERA_FAULT", 105) - CAMERA_LED_FAULT = ("CAMERA_LED_FAULT", 106) - MAIN_BOARD_FAULT = ("MAIN_BOARD_FAULT", 107) - LIDAR_FAULT = ("LIDAR_FAULT", 108) - TRAVEL_MOTOR_LEFT_FRONT_FAULT = ("TRAVEL_MOTOR_LEFT_FRONT_FAULT", 109) - TRAVEL_MOTOR_RIGHT_FRONT_FAULT = ("TRAVEL_MOTOR_RIGHT_FRONT_FAULT", 110) - TRAVEL_MOTOR_LEFT_REAR_FAULT = ("TRAVEL_MOTOR_LEFT_REAR_FAULT", 111) - TRAVEL_MOTOR_RIGHT_REAR_FAULT = ("TRAVEL_MOTOR_RIGHT_REAR_FAULT", 112) - STEERING_MOTOR_LEFT_FRONT_FAULT = ("STEERING_MOTOR_LEFT_FRONT_FAULT", 113) - STEERING_MOTOR_RIGHT_FRONT_FAULT = ("STEERING_MOTOR_RIGHT_FRONT_FAULT", 114) - LED_SCREEN_FAULT = ("LED_SCREEN_FAULT", 115) - LEFT_SIDE_CAMERA_FAULT = ("LEFT_SIDE_CAMERA_FAULT", 116) - RIGHT_SIDE_CAMERA_FAULT = ("RIGHT_SIDE_CAMERA_FAULT", 117) - REAR_SIDE_CAMERA_FAULT = ("REAR_SIDE_CAMERA_FAULT", 118) + """User-facing error code reported by DPS 120.""" + + UNKNOWN = ("unknown", 0) + MAIN_CUTTER_OVER_CURRENT = ("main_cutter_over_current", 1) + MAIN_CUTTER_OVER_TEMPERATURE = ("main_cutter_over_temperature", 2) + MAIN_CUTTER_DYNAMIC_UNBALANCE = ("main_cutter_dynamic_unbalance", 3) + EDGE_CUTTER_OVER_CURRENT = ("edge_cutter_over_current", 4) + EDGE_CUTTER_OVER_TEMPERATURE = ("edge_cutter_over_temperature", 5) + EDGE_CUTTER_DYNAMIC_UNBALANCE = ("edge_cutter_dynamic_unbalance", 6) + HEIGHT_MODULE_ADJUSTING_INCOMPLETE = ("height_module_adjusting_incomplete", 7) + TRAVEL_MOTOR_LEFT_FRONT_OVER_CURRENT = ("travel_motor_left_front_over_current", 8) + TRAVEL_MOTOR_RIGHT_FRONT_OVER_CURRENT = ("travel_motor_right_front_over_current", 9) + TRAVEL_MOTOR_LEFT_REAR_OVER_CURRENT = ("travel_motor_left_rear_over_current", 10) + TRAVEL_MOTOR_RIGHT_REAR_OVER_CURRENT = ("travel_motor_right_rear_over_current", 11) + TRAVEL_MOTOR_OVER_TEMPERATURE = ("travel_motor_over_temperature", 12) + STEERING_MOTOR_LEFT_FRONT_OVER_CURRENT = ("steering_motor_left_front_over_current", 13) + STEERING_MOTOR_RIGHT_FRONT_OVER_CURRENT = ("steering_motor_right_front_over_current", 14) + STEERING_MOTOR_OVER_TEMPERATURE = ("steering_motor_over_temperature", 15) + STOP = ("stop", 16) + TILT = ("tilt", 17) + LIFT = ("lift", 18) + FLIP = ("flip", 19) + OUT_OF_BOUND = ("out_of_bound", 20) + BUMPER = ("bumper", 21) + BATTERY_DISCHARGE_OVER_TEMPERATURE = ("battery_discharge_over_temperature", 22) + BATTERY_DISCHARGE_LOW_TEMPERATURE = ("battery_discharge_low_temperature", 23) + BATTERY_UNOFFICIAL = ("battery_unofficial", 24) + DIRTY_CHARGING_CLIPS = ("dirty_charging_clips", 25) + CHARGE_STATION_NO_POWER = ("charge_station_no_power", 26) + CAMERA_DIRTY_OR_OBSTRUCTED = ("camera_dirty_or_obstructed", 27) + CAMERA_OVER_TEMPERATURE = ("camera_over_temperature", 28) + CAMERA_LED_OBSTRUCTED = ("camera_led_obstructed", 29) + LIDAR_DIRTY = ("lidar_dirty", 30) + LIDAR_OVER_TEMPERATURE = ("lidar_over_temperature", 31) + LOCATION_FAILED = ("location_failed", 32) + LOCATION_AT_ISLAND = ("location_at_island", 33) + LOCATION_AT_NON_COMPLIANT_SLOPE = ("location_at_non_compliant_slope", 34) + BE_TRAPPED = ("be_trapped", 35) + CHARGE_STATION_OR_RTK_SLIGHT_MOVEMENT_DOCK = ("charge_station_or_rtk_slight_movement_dock", 36) + N_RTK_SIGNIFICANT_MOVEMENT = ("n_rtk_significant_movement", 37) + RTK_SIGNIFICANT_MOVEMENT = ("rtk_significant_movement", 38) + DOCK_CHARGE_STATION_SEARCH_FAIL = ("dock_charge_station_search_fail", 39) + DOCK_QRCODE_SCAN_FAIL = ("dock_qrcode_scan_fail", 40) + DOCK_ADJUST_ERROR = ("dock_adjust_error", 41) + UNDOCK_ERROR = ("undock_error", 42) + BATTERY_CHARGE_OVER_TEMPERATURE = ("battery_charge_over_temperature", 43) + BATTERY_CHARGE_LOW_TEMPERATURE = ("battery_charge_low_temperature", 44) + CAMERA_SEVERELY_DIRTY = ("camera_severely_dirty", 45) + NO_PLUGIN_KEY = ("no_plugin_key", 46) + CAMERA_REAR_SIDE_DIRTY = ("camera_rear_side_dirty", 48) + N_RTK_NETWORK_ERROR = ("n_rtk_network_error", 55) + MAIN_CUTTER_FAULT = ("main_cutter_fault", 101) + EDGE_CUTTER_FAULT = ("edge_cutter_fault", 102) + HEIGHT_MODULE_FAULT = ("height_module_fault", 103) + BATTERY_FAULT = ("battery_fault", 104) + CAMERA_FAULT = ("camera_fault", 105) + CAMERA_LED_FAULT = ("camera_led_fault", 106) + MAIN_BOARD_FAULT = ("main_board_fault", 107) + LIDAR_FAULT = ("lidar_fault", 108) + TRAVEL_MOTOR_LEFT_FRONT_FAULT = ("travel_motor_left_front_fault", 109) + TRAVEL_MOTOR_RIGHT_FRONT_FAULT = ("travel_motor_right_front_fault", 110) + TRAVEL_MOTOR_LEFT_REAR_FAULT = ("travel_motor_left_rear_fault", 111) + TRAVEL_MOTOR_RIGHT_REAR_FAULT = ("travel_motor_right_rear_fault", 112) + STEERING_MOTOR_LEFT_FRONT_FAULT = ("steering_motor_left_front_fault", 113) + STEERING_MOTOR_RIGHT_FRONT_FAULT = ("steering_motor_right_front_fault", 114) + LED_SCREEN_FAULT = ("led_screen_fault", 115) + LEFT_SIDE_CAMERA_FAULT = ("left_side_camera_fault", 116) + RIGHT_SIDE_CAMERA_FAULT = ("right_side_camera_fault", 117) + REAR_SIDE_CAMERA_FAULT = ("rear_side_camera_fault", 118) diff --git a/roborock/data/mower/mower_containers.py b/roborock/data/mower/mower_containers.py index ab06ccd71..3413bb399 100644 --- a/roborock/data/mower/mower_containers.py +++ b/roborock/data/mower/mower_containers.py @@ -37,7 +37,7 @@ @dataclass class MowerGpsCoordinate(RoborockBase): - """Mower GPS position (``GpsCoordinateDpValue``).""" + """Mower GPS position.""" latitude: float | None = None longitude: float | None = None @@ -45,24 +45,12 @@ class MowerGpsCoordinate(RoborockBase): @dataclass class MowerStatus(RoborockBase): - """Core mower status backed by mower DPS updates (data points 120-145). - - Absent values mean the device has not reported that data point; for example - a missing battery is unknown, not zero. - """ + """Core mower status backed by mower DPS updates (data points 120-145).""" error_code: MowerErrorCode | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.ERROR_CODE}) battery: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.BATTERY}) - """Battery percentage.""" mow_type: MowerMowType | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_TYPE}) mow_state: MowerDetailState | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_STATE}) - """Task state on data point 123. - - The protocol declares this data point as the compact ``MowStateDpValue`` - domain, but captured devices report the detailed ``RobotDetailState`` - domain here (for example ``58`` = ``MOW_SUSPEND``), so the detailed enum is - used. - """ mapping_type: MowerMappingType | None = field( default=None, metadata={"dps": RoborockMowerDataProtocol.MAPPING_TYPE} ) @@ -84,20 +72,16 @@ class MowerStatus(RoborockBase): ) mow_eff_mode: MowerEffMode | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_EFF_MODE}) mow_height: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_HEIGHT}) - """Reported cutting height in device units.""" mow_direction_angle: int | None = field( default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_DIRECTION_ANGLE} ) - """Reported mowing direction in degrees.""" mow_pattern: MowerPattern | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PATTERN}) mow_conf_mode: MowerConfMode | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_CONF_MODE}) offline_status: MowerOfflineStatus | None = field( default=None, metadata={"dps": RoborockMowerDataProtocol.OFFLINE_STATUS} ) mow_progress: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PROGRESS}) - """Compact task progress in a model-specific scale.""" blade_lifespan: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.BLADE_LIFESPAN}) - """Blade usage or lifespan in reported units.""" fc_state: MowerFcState | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.FC_STATE}) gps_coordinate: MowerGpsCoordinate | None = field( default=None, metadata={"dps": RoborockMowerDataProtocol.GPS_COORDINATE} @@ -112,10 +96,7 @@ class MowerStatus(RoborockBase): @classmethod def from_dps(cls, dps: dict[int | str, Any] | None) -> Self: - """Create typed status from integer or string DPS keys. - - Unknown and malformed keys are ignored, matching the existing factory. - """ + """Create typed status from integer or string DPS keys.""" field_by_dps = { int(field_info.metadata["dps"]): field_info.name for field_info in fields(cls) diff --git a/tests/data/mower/test_mower_code_mappings.py b/tests/data/mower/test_mower_code_mappings.py index ff87a6c39..4f36bfe26 100644 --- a/tests/data/mower/test_mower_code_mappings.py +++ b/tests/data/mower/test_mower_code_mappings.py @@ -16,7 +16,6 @@ MowerNetworkChannel, MowerOfflineStatus, MowerPendType, - RoborockMowerStateCode, ) ALL_MOWER_ENUMS: list[type[RoborockModeEnum]] = sorted( @@ -36,10 +35,12 @@ def test_all_mower_enums_are_exported() -> None: @pytest.mark.parametrize("enum_cls", ALL_MOWER_ENUMS, ids=lambda cls: cls.__name__) -def test_mower_enum_values_are_protobuf_names(enum_cls: type[RoborockModeEnum]) -> None: - """The string value is the protobuf enum name, so it can round-trip through the JSON route.""" +def test_mower_enum_values_are_lowercase_and_round_trip(enum_cls: type[RoborockModeEnum]) -> None: + """String values are lowercase and round-trip by value, uppercase protobuf name, and integer code.""" for member in enum_cls: + assert member.value == member.value.lower() assert enum_cls.from_value(member.value) is member + assert enum_cls.from_value(member.value.upper()) is member assert enum_cls.from_code(member.code) is member @@ -103,8 +104,8 @@ def test_mower_enum_unknown_codes_resolve_to_none(enum_cls: type[RoborockModeEnu ], ) def test_misspelled_wire_names_are_preserved(member: RoborockModeEnum, wire_name: str) -> None: - """Protocol misspellings are kept as the value so the JSON route still matches.""" - assert member.value == wire_name + """Protocol misspellings are kept as the lowercased value so the JSON route still matches.""" + assert member.value == wire_name.lower() assert type(member).from_value(wire_name) is member @@ -112,6 +113,7 @@ def test_misspelled_wire_names_are_preserved(member: RoborockModeEnum, wire_name ("enum_cls", "raw", "expected"), [ (MowerEffMode, "DAILY", MowerEffMode.DAILY), + (MowerEffMode, "daily", MowerEffMode.DAILY), (MowerEffMode, 1, MowerEffMode.DAILY), (MowerEffMode, "1", MowerEffMode.DAILY), (MowerDetailState, "MAP_ERASEING", MowerDetailState.MAP_ERASING), @@ -125,24 +127,3 @@ def test_from_code_accepts_wire_forms_and_falls_back( enum_cls: type[MowerEnum], raw: int | str, expected: MowerEnum ) -> None: assert enum_cls.from_code(raw) is expected - - -@pytest.mark.parametrize( - ("code", "expected"), - [ - (0, RoborockMowerStateCode.idle), - (12, RoborockMowerStateCode.map_erasing), - (58, RoborockMowerStateCode.mow_suspend), - (61, RoborockMowerStateCode.mow_docked_rainfall), - (151, RoborockMowerStateCode.charge_charging), - (154, RoborockMowerStateCode.charge_fault), - (9999, RoborockMowerStateCode.unknown), - ], -) -def test_legacy_state_enum_remains_available(code: int, expected: RoborockMowerStateCode) -> None: - """Existing imports, lower-case member names and integer lookups still work.""" - from roborock.data import RoborockMowerStateCode as exported_enum - - assert exported_enum is RoborockMowerStateCode - assert exported_enum(code) is expected - assert exported_enum.unknown.value == -999 diff --git a/tests/data/mower/test_mower_containers.py b/tests/data/mower/test_mower_containers.py index 13221bad6..5cbbb943a 100644 --- a/tests/data/mower/test_mower_containers.py +++ b/tests/data/mower/test_mower_containers.py @@ -57,8 +57,8 @@ def test_mower_status_round_trips_through_as_dict() -> None: serialized = status.as_dict() - assert serialized["mowState"] == "MOW_SUSPEND" - assert serialized["mowType"] == "GLOBAL" + assert serialized["mowState"] == "mow_suspend" + assert serialized["mowType"] == "global" assert MowerStatus.from_dict(serialized) == status