From a1795f545f36905650a0cdad4fcefc7406950940 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Mon, 9 Nov 2026 21:00:15 -0500 Subject: [PATCH] feat(mower): add structured command and reply models --- roborock/data/mower/__init__.py | 1 + roborock/data/mower/mower_code_mappings.py | 677 +++++++++++++++++++ roborock/data/mower/mower_commands.py | 489 ++++++++++++++ tests/data/mower/test_mower_code_mappings.py | 24 + tests/data/mower/test_mower_commands.py | 347 ++++++++++ 5 files changed, 1538 insertions(+) create mode 100644 roborock/data/mower/mower_commands.py create mode 100644 tests/data/mower/test_mower_commands.py diff --git a/roborock/data/mower/__init__.py b/roborock/data/mower/__init__.py index 5f4d2ce41..f86920c12 100644 --- a/roborock/data/mower/__init__.py +++ b/roborock/data/mower/__init__.py @@ -1,4 +1,5 @@ """Dataclasses for Roborock mower devices.""" from .mower_code_mappings import * +from .mower_commands import * from .mower_containers import * diff --git a/roborock/data/mower/mower_code_mappings.py b/roborock/data/mower/mower_code_mappings.py index 26adbe9fc..d73efcc38 100644 --- a/roborock/data/mower/mower_code_mappings.py +++ b/roborock/data/mower/mower_code_mappings.py @@ -30,6 +30,28 @@ "MowerPendType", "MowerRemoteState", "MowerStartType", + "MowerRemoteMsgType", + "MowerRobotMsgType", + "MowerAppButton", + "MowerFsmEvent", + "MowerFsmState", + "MowerAckResult", + "MowerPreferenceEffective", + "MowerDirectionType", + "MowerBoundaryPerception", + "MowerPreferenceMode", + "MowerPlanType", + "MowerPlanStatus", + "MowerWeekday", + "MowerRainType", + "MowerNavSolution", + "MowerDriveMode", + "MowerMobile4GMode", + "MowerSkuRegion", + "MowerStartDpType", + "MowerFeature", + "MowerMcuType", + "MowerRtkMode", ] @@ -400,3 +422,658 @@ class MowerErrorCode(MowerEnum): 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) + + +class MowerRemoteMsgType(MowerEnum): + """Structured command type sent in `RemoteMsg.type` (`rock.common.remote.RemoteMsg.Type`).""" + + UNKNOWN = ("unknown", 0) + ACK = ("ack", 1) + GET_FULL_MAP = ("get_full_map", 2) + GET_ROBOT_STATUS = ("get_robot_status", 3) + GET_SELF_CHECK_STATUS = ("get_self_check_status", 4) + REMOTE_CONTROL = ("remote_control", 5) + APP_BUTTON = ("app_button", 6) + START_GET_INC_MAP = ("start_get_inc_map", 7) + STOP_GET_INC_MAP = ("stop_get_inc_map", 8) + GET_MAP_DIFF = ("get_map_diff", 9) + GOTO_TARGET = ("goto_target", 10) + GET_MAP_NAMES = ("get_map_names", 11) + DELETE_MAP = ("delete_map", 12) + GET_MAP_DIFFS = ("get_map_diffs", 13) + PIN_CODE = ("pin_code", 14) + LORA = ("lora", 15) + PLAY_AUDIO = ("play_audio", 16) + REMOTE_CMD = ("remote_cmd", 17) + REMOTE_INFO_REQUEST = ("remote_info_request", 18) + MOW_RECORD_REQUEST = ("mow_record_request", 19) + SET_MOW_PREFERENCE = ("set_mow_preference", 20) + DELETE_SINGLE_MOW_RECORD = ("delete_single_mow_record", 21) + GET_SINGLE_DETAIL_MOW_RECORD = ("get_single_detail_mow_record", 22) + SET_DEBUG_PARAMETER = ("set_debug_parameter", 23) + GET_DEBUG_PARAMETER = ("get_debug_parameter", 24) + CHANGE_MOW_PREFERENCE = ("change_mow_preference", 25) + GET_MOW_PREFERENCE_CONFIG = ("get_mow_preference_config", 26) + SET_NOT_DISTURB = ("set_not_disturb", 27) + SET_RAINFALL = ("set_rainfall", 28) + GET_USER_MODE_CONFIG = ("get_user_mode_config", 29) + SET_AI_OBSTACLE_AVOIDANCE = ("set_ai_obstacle_avoidance", 30) + GET_CURRENT_MOW_RECORDER = ("get_current_mow_recorder", 31) + GET_FEATURESET = ("get_featureset", 32) + GET_MAP_MOW_SNAPSHOT = ("get_map_mow_snapshot", 33) + GET_HEIGHT_MOTOR_PARAMETER = ("get_height_motor_parameter", 34) + DP_POINTS = ("dp_points", 35) + CREATE_MOWING_PLAN = ("create_mowing_plan", 36) + CHANGE_MOWING_PLAN = ("change_mowing_plan", 37) + DELETE_MOWING_PLAN = ("delete_mowing_plan", 38) + GET_MOW_SCHEDULE = ("get_mow_schedule", 39) + DELETE_MOW_SCHEDULE = ("delete_mow_schedule", 40) + GET_ROBOT_TIME_ZONE = ("get_robot_time_zone", 41) + BIT_CHECK = ("bit_check", 42) + MIDNIGHT_RESET = ("midnight_reset", 43) + CHANGE_RUNTIME_TASK = ("change_runtime_task", 44) + FIND_ME = ("find_me", 45) + # The protocol spelling "GET_BlACK_BOX" is intentional. + GET_BLACK_BOX = ("get_black_box", 46) + SET_E_FENCE_RANGE = ("set_e_fence_range", 47) + SET_ANTI_THEFT = ("set_anti_theft", 48) + EDIT_MAP = ("edit_map", 49) + GET_MAP_SELF_CHECK_RESULT = ("get_map_self_check_result", 50) + SET_AUDIO = ("set_audio", 51) + INJECT_ERRORS = ("inject_errors", 52) + GET_E_FENCE_AREA = ("get_e_fence_area", 53) + CLEAR_CACHE_FSM_EVENT = ("clear_cache_fsm_event", 54) + GET_SKU_INFO = ("get_sku_info", 55) + GET_BIT_HISTORY = ("get_bit_history", 56) + SET_AP_BOARD_SN = ("set_ap_board_sn", 57) + SET_NETWORK_MODE = ("set_network_mode", 58) + UPDATE_TIMEZONE = ("update_timezone", 59) + GET_SW_VERSION = ("get_sw_version", 60) + GET_BIT_CHECK_LISTS = ("get_bit_check_lists", 61) + GET_REMOTE_MOW_SELF_CHECK_RESULT = ("get_remote_mow_self_check_result", 62) + SWITCH_TO_CUSTOM_AUDIO = ("switch_to_custom_audio", 63) + GET_MCU_VERSION = ("get_mcu_version", 64) + GET_PROJECT = ("get_project", 65) + GET_ROBOT_INFO = ("get_robot_info", 66) + GET_ROBOT_NETWORK_INFO = ("get_robot_network_info", 67) + GET_ROBOT_BLUETOOTH_INFO = ("get_robot_bluetooth_info", 68) + GET_ESIM_ACTIVE_STATUS = ("get_esim_active_status", 69) + ACTIVE_ESIM = ("active_esim", 70) + GET_ESIM_ACTIVE_RESULT = ("get_esim_active_result", 71) + GET_ESIM_INFO = ("get_esim_info", 72) + QUERY_SKU_CONFIG_STATUS = ("query_sku_config_status", 73) + DOWNLOAD_SKU_CONFIG = ("download_sku_config", 74) + SET_NAV_COMMON_CONFIG = ("set_nav_common_config", 75) + GET_BOOT_INFO = ("get_boot_info", 76) + GET_MOWING_ZONES = ("get_mowing_zones", 77) + QUERY_BOARD_SN_INFO = ("query_board_sn_info", 78) + UPDATE_BOARD_SN_INFO = ("update_board_sn_info", 79) + GET_FEATURE_INFO = ("get_feature_info", 80) + CLEAR_MOW_TASK = ("clear_mow_task", 90) + GET_MIDNIGHT_RESET_FROM_ROBOT = ("get_midnight_reset_from_robot", 91) + APP_BUTTON_WITH_MAP_SELF_CHECK_RESULT = ("app_button_with_map_self_check_result", 92) + GET_SERIAL_NUMBER = ("get_serial_number", 93) + USER_UPLOAD_LOG = ("user_upload_log", 94) + SET_FDS_ENDPOINT = ("set_fds_endpoint", 95) + ENABLE_LOG_UPLOAD = ("enable_log_upload", 96) + APP_GET_INIT_STATUS = ("app_get_init_status", 97) + GET_CURRENT_SOUND = ("get_current_sound", 98) + DNLD_INSTALL_SOUND = ("dnld_install_sound", 99) + GET_SOUND_PROGRESS = ("get_sound_progress", 100) + APP_GET_WIFI_LIST = ("app_get_wifi_list", 101) + APP_DELETE_WIFI = ("app_delete_wifi", 102) + DISABLE_LOG_UPLOAD = ("disable_log_upload", 103) + APP_GET_LOCALE = ("app_get_locale", 104) + GET_INSTALLATION_WIZARD = ("get_installation_wizard", 105) + SET_INSTALLATION_WIZARD = ("set_installation_wizard", 106) + GET_TEST_ID = ("get_test_id", 107) + GET_ZONES_PLAN_INFO = ("get_zones_plan_info", 108) + SET_MOW_PREFERENCE_CONFIG = ("set_mow_preference_config", 109) + GET_FAULT_RECORDS = ("get_fault_records", 110) + GET_MAP_ABSTRACTS = ("get_map_abstracts", 111) + UPDATE_GPS_INFO = ("update_gps_info", 112) + APP_PROXY_INIT_OK = ("app_proxy_init_ok", 113) + REQUEST_MAINTENANCE_STATUS = ("request_maintenance_status", 114) + RESET_MAINTENANCE = ("reset_maintenance", 115) + MAINTENANCE_REMINDER = ("maintenance_reminder", 116) + CLEAR_REGION_PROGRESS = ("clear_region_progress", 117) + GET_LOG_LTE_EVENTS = ("get_log_lte_events", 118) + SET_LOG_LTE_REQUEST = ("set_log_lte_request", 119) + GET_LOG_LTE_UPLOAD_INFO = ("get_log_lte_upload_info", 120) + LOG_LTE_ACTIVATE = ("log_lte_activate", 121) + WAKEUP_ROBOT = ("wakeup_robot", 122) + EMC_MODE_REQUEST = ("emc_mode_request", 123) + EMC_MODE_CONTROL = ("emc_mode_control", 124) + EMC_MODE_INFO = ("emc_mode_info", 125) + EMC_MODE_REQUEST_INFO = ("emc_mode_request_info", 126) + SET_CUSTOM_MOW_PREFERENCE = ("set_custom_mow_preference", 127) + GET_RTK_DEVICE_TYPE = ("get_rtk_device_type", 128) + TRY_SWITCH_LTE_TYPE = ("try_switch_lte_type", 129) + SIM_INSERT_TO_RC = ("sim_insert_to_rc", 130) + SIM_ABUSE_TO_RC = ("sim_abuse_to_rc", 131) + GET_SWITCH_LTE_RESULT = ("get_switch_lte_result", 132) + SET_REMOTE_MOW_CONFIG = ("set_remote_mow_config", 133) + SET_GRASS_ROPE = ("set_grass_rope", 134) + NRTK_MSG_TO_MOWER = ("nrtk_msg_to_mower", 135) + NRTK_ACCOUNT_STATUS = ("nrtk_account_status", 136) + NRTK_APP_CONNECT = ("nrtk_app_connect", 137) + GET_DOCK_LIST = ("get_dock_list", 138) + SET_DOCK_PAIR = ("set_dock_pair", 139) + GET_DOCK_PAIR_RESULT = ("get_dock_pair_result", 140) + GET_DOCK_PAIRED_STATUS = ("get_dock_paired_status", 141) + REMOVE_DOCK_PAIR = ("remove_dock_pair", 142) + UPDATE_DOCK_PAIR = ("update_dock_pair", 143) + SET_REALTIME_VIDEO = ("set_realtime_video", 144) + GET_REALTIME_VIDEO = ("get_realtime_video", 145) + SET_REALTIME_VIDEO_CONFIG = ("set_realtime_video_config", 146) + MAP_FILE_REQUEST = ("map_file_request", 147) + GET_PANORAMA_MAP_ABSTRACTS = ("get_panorama_map_abstracts", 148) + GET_DOCK_CONNECT_STATUS = ("get_dock_connect_status", 149) + SET_LIFT_ALARM_ENABLE = ("set_lift_alarm_enable", 150) + DOCK_LORA_INIT = ("dock_lora_init", 151) + SET_CHILD_LOCK_ENABLE = ("set_child_lock_enable", 152) + NRTK_CLOUD_CONNECT = ("nrtk_cloud_connect", 153) + GET_S1G_INFO_LIST = ("get_s1g_info_list", 160) + CONNECT_TO_S1G = ("connect_to_s1g", 161) + GET_S1G_SCAN_WIFI_LIST = ("get_s1g_scan_wifi_list", 162) + SET_S1G_PAIR = ("set_s1g_pair", 163) + REMOVE_S1G_PAIR = ("remove_s1g_pair", 164) + GET_S1G_PAIR_STATUS = ("get_s1g_pair_status", 165) + SAVE_S1G_IOT_CONFIG = ("save_s1g_iot_config", 166) + + +class MowerRobotMsgType(MowerEnum): + """Reply or event type in `RobotMsg.type` (`rock.common.RobotMsg.Type`).""" + + UNKNOWN = ("unknown", 0) + ACK = ("ack", 1) + MAP_DATA = ("map_data", 2) + MAP_NAME = ("map_name", 3) + MAP_NAMES = ("map_names", 4) + ROBOT_STATUS = ("robot_status", 5) + SELF_CHECK_STATUS = ("self_check_status", 6) + SLAM_STATUS = ("slam_status", 7) + NAVIGATION_STATUS = ("navigation_status", 8) + CLEAR_MAP_DIFF = ("clear_map_diff", 9) + MAP_UPDATE = ("map_update", 10) + WATCHDOG_UPDATE_RESULT = ("watchdog_update_result", 11) + SYSUPD_RESULT = ("sysupd_result", 12) + FACTORY_RESET = ("factory_reset", 13) + CLEAR_INC_POSES = ("clear_inc_poses", 14) + REMOTE_MODE_INFO = ("remote_mode_info", 15) + ROBOT_ERROR = ("robot_error", 16) + RECORD_MOW_TRAJECTORY = ("record_mow_trajectory", 17) + UNRECORD_MOW_TRAJECTORY = ("unrecord_mow_trajectory", 18) + CLEAR_MOW_TRAJECTORY = ("clear_mow_trajectory", 19) + MOW_RECORD = ("mow_record", 20) + MOW_PROGRESS = ("mow_progress", 21) + ALL_MOW_RECORDERS = ("all_mow_recorders", 22) + DEBUG_PARAMETER = ("debug_parameter", 23) + MOW_PREFERENCE_CONFIG = ("mow_preference_config", 24) + USER_MODE_CONFIG = ("user_mode_config", 25) + CURRENT_MOW_RECORDER = ("current_mow_recorder", 26) + POPUP = ("popup", 27) + FEATURESET = ("featureset", 28) + RECORD_SNAPSHOT_MAP = ("record_snapshot_map", 29) + HEIGHT_MOTOR_PARAMETER = ("height_motor_parameter", 30) + DP_POINTS = ("dp_points", 31) + PUSH_NOTIFICATION = ("push_notification", 32) + LORA_PAIR_RESULT = ("lora_pair_result", 33) + MOW_SCHEDULE = ("mow_schedule", 34) + ROBOT_TIME_ZONE = ("robot_time_zone", 35) + BIT_RESULTS = ("bit_results", 36) + BLACK_BOX_RESULTS = ("black_box_results", 37) + ROBOT_STATUS_UPDATE = ("robot_status_update", 38) + MAP_SELF_CHECK_RESULT = ("map_self_check_result", 39) + ROBOT_GPS_UPLOAD = ("robot_gps_upload", 40) + MMT_RESULTS = ("mmt_results", 41) + SKU_INFO = ("sku_info", 42) + BIT_HISTORY = ("bit_history", 43) + MMT_ABNORMAL_EXIT = ("mmt_abnormal_exit", 44) + SW_VERSION = ("sw_version", 45) + RESTART_BIT_RESULTS = ("restart_bit_results", 46) + BIT_CHECK_LISTS = ("bit_check_lists", 47) + REMOTE_MOW_SELF_CHECK_RESULT = ("remote_mow_self_check_result", 48) + MCU_VERSION = ("mcu_version", 49) + PROJECT = ("project", 50) + BROADCAST_FSM_EVENT = ("broadcast_fsm_event", 51) + ROBOT_INFO = ("robot_info", 52) + ROBOT_NETWORK_INFO = ("robot_network_info", 53) + ROBOT_BLUETOOTH_INFO = ("robot_bluetooth_info", 54) + ESIM_ACTIVE_STATUS = ("esim_active_status", 55) + ESIM_ACTIVE_RESULT = ("esim_active_result", 56) + ESIM_INFO = ("esim_info", 57) + SKU_CONFIG = ("sku_config", 58) + AFS_CALIBRATION_RES = ("afs_calibration_res", 59) + MOWING_ZONES = ("mowing_zones", 60) + QUERY_BOARD_SN_RES = ("query_board_sn_res", 61) + SPARE_BOARD_SN_RES = ("spare_board_sn_res", 62) + MAP_CREATE_FAIL = ("map_create_fail", 63) + FEATURE_INFO = ("feature_info", 64) + MIDNIGHT_RESET_TO_APP_PROXY = ("midnight_reset_to_app_proxy", 65) + ROBOT_BUMPER_ACTIVE_ONCE = ("robot_bumper_active_once", 66) + SERIAL_NUMBER = ("serial_number", 67) + INIT_STATUS = ("init_status", 68) + CURRENT_SOUND = ("current_sound", 69) + DNLD_INSTALL_SOUND_RESULT = ("dnld_install_sound_result", 70) + SOUND_PROGRESS = ("sound_progress", 71) + WIFI_LIST = ("wifi_list", 72) + LOCAL_INFO = ("local_info", 73) + APP_RESPONSE_ERROR = ("app_response_error", 74) + INSTALLATION_WIZARD = ("installation_wizard", 75) + TEST_ID = ("test_id", 76) + ZONES_PLAN_INFO = ("zones_plan_info", 77) + MAP_CREATE_STATE = ("map_create_state", 78) + BATTERY_PERCENT = ("battery_percent", 79) + FAULT_RECORDS = ("fault_records", 80) + MAP_ABSTRACTS = ("map_abstracts", 81) + SET_SERVO_CALIB = ("set_servo_calib", 82) + MAINTENANCE_STATUS = ("maintenance_status", 83) + RESET_MAINTENANCE = ("reset_maintenance", 84) + MOW_TRAJECTORY_EVENT = ("mow_trajectory_event", 85) + AFS_LINE_DIFF_CHECK_RES = ("afs_line_diff_check_res", 86) + AFS_LINE_DIFF_CALIBRATION_RES = ("afs_line_diff_calibration_res", 87) + LOG_LTE_EVENTS = ("log_lte_events", 88) + LOG_LTE_UPLOAD_INFO = ("log_lte_upload_info", 89) + LORA_CONFIG_RESULT = ("lora_config_result", 90) + ROBO_EMC_MODE_REQUEST = ("robo_emc_mode_request", 91) + ROBO_EMC_DEVICE_INFO = ("robo_emc_device_info", 92) + RTK_DEVICE_TYPE = ("rtk_device_type", 93) + SWITCH_LTE_RESULT = ("switch_lte_result", 94) + NRTK_REQUEST = ("nrtk_request", 95) + NRTK_RETURN = ("nrtk_return", 96) + NRTK_SYNCHRONIZATION = ("nrtk_synchronization", 97) + RTK_MODE = ("rtk_mode", 98) + NRTK_MSG_TO_APP = ("nrtk_msg_to_app", 99) + DOCK_LIST_INFO = ("dock_list_info", 100) + DOCK_PAIR_INFO = ("dock_pair_info", 101) + DOCK_PAIRED_STATUS = ("dock_paired_status", 102) + SET_DOCK_CHARGING_TYPE = ("set_dock_charging_type", 103) + REALTIME_VIDEO = ("realtime_video", 104) + PANORAMA_MAP_ABSTRACTS = ("panorama_map_abstracts", 105) + MAP_TRAJECTORY_EVENT = ("map_trajectory_event", 106) + LIFT_ALARM_ENABLE = ("lift_alarm_enable", 107) + DOCK_LORA_CONFIG_RESULT = ("dock_lora_config_result", 108) + NRTK_SEND = ("nrtk_send", 109) + NRTK_RECYCLE = ("nrtk_recycle", 110) + CHILD_LOCK_ENABLE = ("child_lock_enable", 111) + S1G_INFO_LIST = ("s1g_info_list", 120) + S1G_SCAN_WIFI_LIST = ("s1g_scan_wifi_list", 121) + S1G_PAIR_STATUS = ("s1g_pair_status", 122) + + +class MowerAppButton(MowerEnum): + """Button sent with `RemoteMsg.type = APP_BUTTON` (`rock.common.remote.AppButton.Type`).""" + + UNKNOWN = ("unknown", -1) + NONE = ("none", 0) + START = ("start", 1) + PAUSE = ("pause", 2) + STOP = ("stop", 3) + RESUME = ("resume", 4) + CHARGE = ("charge", 5) + CREATE_MAP = ("create_map", 6) + RESUME_CREATE_MAP = ("resume_create_map", 7) + TO_CLOSE = ("to_close", 8) + ERASE = ("erase", 9) + EXIT_MAPPING_PAGE = ("exit_mapping_page", 10) + BUILD_BOUNDARY = ("build_boundary", 11) + BUILD_ISLAND = ("build_island", 12) + BUILD_PATH = ("build_path", 13) + MOW_GLOBAL = ("mow_global", 14) + MOW_EDGE = ("mow_edge", 15) + MOW_SELECT = ("mow_select", 16) + DOCK_START = ("dock_start", 17) + SAVE_MAP = ("save_map", 18) + MAP_PAUSE = ("map_pause", 19) + MOW_PAUSE = ("mow_pause", 20) + MAP_RESUME = ("map_resume", 21) + MOW_RESUME = ("mow_resume", 22) + MAP_END = ("map_end", 23) + MOW_END = ("mow_end", 24) + DOCK_END = ("dock_end", 25) + PREPARE_BUILD_PATH = ("prepare_build_path", 26) + BUILD_BOUNDARY_AUTO = ("build_boundary_auto", 27) + PREPARE_BUILD_BOUNDARY = ("prepare_build_boundary", 28) + PREPARE_BUILD_ISLAND = ("prepare_build_island", 29) + MAP_EDIT_ENTER = ("map_edit_enter", 30) + MAP_EDIT_EXIT = ("map_edit_exit", 31) + MAP_TERMINATE = ("map_terminate", 32) + STOP_BIT_TASK = ("stop_bit_task", 33) + ANTI_THEFT_ENABLE = ("anti_theft_enable", 34) + ANTI_THEFT_DISABLE = ("anti_theft_disable", 35) + STOP_MMT_TASK = ("stop_mmt_task", 36) + TRIGGER_MIDNIGHT_RESET_ONCE = ("trigger_midnight_reset_once", 37) + STOP_AFS_CALIBRATION_TASK = ("stop_afs_calibration_task", 38) + CLEAR_ERROR = ("clear_error", 39) + CHARGE_WITH_NORMAL_MOW_RESERVE = ("charge_with_normal_mow_reserve", 40) + CHARGE_WITH_PLAN_MOW_RESERVE = ("charge_with_plan_mow_reserve", 41) + RESUME_PREPARE_BUILD = ("resume_prepare_build", 42) + MOW_AUTO_TO_REMOTE = ("mow_auto_to_remote", 43) + DP_CHARGE = ("dp_charge", 44) + CLEAR_ONLY_APP_ERROR = ("clear_only_app_error", 45) + # The protocol spelling "ClEAR_ROBOT_AND_APP_ERROR" is intentional. + CLEAR_ROBOT_AND_APP_ERROR = ("clear_robot_and_app_error", 46) + ERROR_RETRY = ("error_retry", 47) + CLEAR_MAP_KEY = ("clear_map_key", 48) + TO_DOCK_FAULT_CHARGE = ("to_dock_fault_charge", 49) + RESET_UNDOCK = ("reset_undock", 50) + BUILD_NO_AVOID = ("build_no_avoid", 51) + PREPARE_BUILD_NO_AVOID = ("prepare_build_no_avoid", 52) + RESET_PASSAGE = ("reset_passage", 53) + PREPARE_RESET_PASSAGE = ("prepare_reset_passage", 54) + EDIT_BOUNDARY = ("edit_boundary", 55) + PREPARE_EDIT_BOUNDARY = ("prepare_edit_boundary", 56) + CONFIRM_EDIT_BOUNDARY = ("confirm_edit_boundary", 57) + HUMAN_BIT_DONE = ("human_bit_done", 100) + SCREEN_CHECK_PASS = ("screen_check_pass", 101) + SCREEN_CHECK_FAILED = ("screen_check_failed", 102) + GET_BIT_CHECK_INFOS = ("get_bit_check_infos", 103) + SERVO_CALIB_START = ("servo_calib_start", 104) + SERVO_START_VALIDATION = ("servo_start_validation", 105) + SERVO_MOVE_LEFT_45 = ("servo_move_left_45", 106) + SERVO_MOVE_RIGHT_45 = ("servo_move_right_45", 107) + SERVO_RESET_0 = ("servo_reset_0", 108) + SERVO_END_CALIBRATION = ("servo_end_calibration", 109) + START_AFS_LINE_DIFF_CHECK = ("start_afs_line_diff_check", 110) + STOP_AFS_LINE_DIFF_CHECK = ("stop_afs_line_diff_check", 111) + START_AFS_LINE_DIFF_CALIBRATION = ("start_afs_line_diff_calibration", 112) + STOP_AFS_LINE_DIFF_CALIBRATION = ("stop_afs_line_diff_calibration", 113) + RETURN_TO_RUNTIME_MENU = ("return_to_runtime_menu", 114) + DETECT_CHARGE_STATION = ("detect_charge_station", 115) + + +class MowerFsmEvent(MowerEnum): + """Mapping-preparation events for `button_with_map_selfcheck_result`.""" + + UNKNOWN = ("unknown", -1) + APP_BUTTON_START = ("app_button_start", 301) + APP_BUTTON_PAUSE = ("app_button_pause", 302) + APP_BUTTON_STOP = ("app_button_stop", 303) + APP_BUTTON_RESUME = ("app_button_resume", 304) + APP_BUTTON_CHARGE = ("app_button_charge", 305) + APP_BUTTON_CREATE_MAP = ("app_button_create_map", 306) + APP_BUTTON_RESUME_CREATE_MAP = ("app_button_resume_create_map", 307) + APP_BUTTON_TO_CLOSE = ("app_button_to_close", 308) + APP_BUTTON_ERASE = ("app_button_erase", 309) + APP_BUTTON_EXIT_MAPPING_PAGE = ("app_button_exit_mapping_page", 310) + APP_BUTTON_BUILD_BOUNDARY = ("app_button_build_boundary", 311) + APP_BUTTON_BUILD_ISLAND = ("app_button_build_island", 312) + APP_BUTTON_BUILD_PATH = ("app_button_build_path", 313) + APP_BUTTON_MOW_GLOBAL = ("app_button_mow_global", 314) + APP_BUTTON_MOW_EDGE = ("app_button_mow_edge", 315) + APP_BUTTON_MOW_SELECT = ("app_button_mow_select", 316) + APP_BUTTON_DOCK_START = ("app_button_dock_start", 317) + APP_BUTTON_SAVE_MAP = ("app_button_save_map", 318) + APP_BUTTON_MAP_PAUSE = ("app_button_map_pause", 319) + APP_BUTTON_MOW_PAUSE = ("app_button_mow_pause", 320) + APP_BUTTON_MAP_RESUME = ("app_button_map_resume", 321) + APP_BUTTON_MOW_RESUME = ("app_button_mow_resume", 322) + APP_BUTTON_MAP_END = ("app_button_map_end", 323) + APP_BUTTON_MOW_END = ("app_button_mow_end", 324) + APP_BUTTON_DOCK_END = ("app_button_dock_end", 325) + APP_BUTTON_PREPARE_BUILD_PATH = ("app_button_prepare_build_path", 326) + APP_BUTTON_BUILD_BOUNDARY_AUTO = ("app_button_build_boundary_auto", 327) + APP_BUTTON_PREPARE_BUILD_BOUNDARY = ("app_button_prepare_build_boundary", 328) + APP_BUTTON_PREPARE_BUILD_ISLAND = ("app_button_prepare_build_island", 329) + APP_BUTTON_MAP_EDIT_ENTER = ("app_button_map_edit_enter", 330) + APP_BUTTON_MAP_EDIT_EXIT = ("app_button_map_edit_exit", 331) + APP_BUTTON_MAP_TERMINATE = ("app_button_map_terminate", 332) + APP_BUTTON_STOP_BIT_TASK = ("app_button_stop_bit_task", 333) + APP_BUTTON_ANTI_THEFT_ENABLE = ("app_button_anti_theft_enable", 334) + APP_BUTTON_ANTI_THEFT_DISABLE = ("app_button_anti_theft_disable", 335) + APP_BUTTON_STOP_MMT_TASK = ("app_button_stop_mmt_task", 336) + APP_BUTTON_TRIGGER_MIDNIGHT_RESET_ONCE = ("app_button_trigger_midnight_reset_once", 337) + APP_BUTTON_STOP_AFS_CALIBRATION_TASK = ("app_button_stop_afs_calibration_task", 338) + APP_BUTTON_CLEAR_ERROR = ("app_button_clear_error", 339) + APP_BUTTON_CHARGE_WITH_NORMAL_MOW_RESERVE = ("app_button_charge_with_normal_mow_reserve", 340) + APP_BUTTON_CHARGE_WITH_PLAN_MOW_RESERVE = ("app_button_charge_with_plan_mow_reserve", 341) + APP_BUTTON_RESUME_PREPARE_BUILD = ("app_button_resume_prepare_build", 342) + APP_BUTTON_DP_CHARGE = ("app_button_dp_charge", 343) + APP_BUTTON_TO_DOCK_FAULT_CHARGE = ("app_button_to_dock_fault_charge", 344) + APP_BUTTON_STOP_AFS_LINE_DIFF_CALIBRATION_TASK = ("app_button_stop_afs_line_diff_calibration_task", 345) + APP_BUTTON_RETURN_TO_RUNTIME_MENU_AFS = ("app_button_return_to_runtime_menu_afs", 346) + APP_BUTTON_RESET_UNDOCK = ("app_button_reset_undock", 347) + APP_BUTTON_BUILD_NO_AVOID = ("app_button_build_no_avoid", 348) + APP_BUTTON_PREPARE_BUILD_NO_AVOID = ("app_button_prepare_build_no_avoid", 349) + APP_BUTTON_RESET_PASSAGE = ("app_button_reset_passage", 350) + APP_BUTTON_PREPARE_RESET_PASSAGE = ("app_button_prepare_reset_passage", 351) + APP_BUTTON_EDIT_BOUNDARY = ("app_button_edit_boundary", 352) + APP_BUTTON_PREPARE_EDIT_BOUNDARY = ("app_button_prepare_edit_boundary", 353) + APP_BUTTON_CONFIRM_EDIT_BOUNDARY = ("app_button_confirm_edit_boundary", 354) + + +class MowerFsmState(MowerEnum): + """Task-machine state (`rock.fsm.FsmState.Type`).""" + + UNKNOWN = ("unknown", 0) + IDLE = ("idle", 1) + FAULT = ("fault", 2) + SUSPEND = ("suspend", 3) + CHARGING = ("charging", 4) + CHARGE_COMPLETED = ("charge_completed", 5) + CHARGE_WAITING = ("charge_waiting", 6) + RETURN = ("return", 7) + DOWNLOADING = ("downloading", 8) + INSTALLING = ("installing", 9) + REBOOTING = ("rebooting", 10) + GOTO_TARGET = ("goto_target", 11) + BUILDING_BOUNDARY = ("building_boundary", 12) + BUILDING_ISLAND = ("building_island", 13) + BUILDING_PATH = ("building_path", 14) + SAVE = ("save", 15) + ERASING = ("erasing", 16) + SWITCH_SCENES = ("switch_scenes", 17) + MOW_GLOBAL = ("mow_global", 18) + MOW_EDGE = ("mow_edge", 19) + MOW_SELECT = ("mow_select", 20) + AT = ("at", 21) + STANDBY = ("standby", 22) + RUN = ("run", 23) + EMERGENCY_STOP = ("emergency_stop", 24) + + +class MowerAckResult(MowerEnum): + """Request acceptance result (`rock.common.Ack.Type`).""" + + UNKNOWN = ("unknown", -1) + OK = ("ok", 0) + FAIL = ("fail", 1) + + +class MowerPreferenceEffective(MowerEnum): + """Mowing-efficiency preference (`MowPreference.Effective`).""" + + UNKNOWN = ("unknown", 0) + DAILY = ("daily", 1) + EFFICIENT = ("efficient", 2) + MANICURE = ("manicure", 3) + CUSTOM_MOW = ("custom_mow", 4) + + +class MowerDirectionType(MowerEnum): + """Mowing direction mode (`MowPreference.DirectionType`).""" + + UNKNOWN = ("unknown", -1) + NONE = ("none", 0) + CUSTOM = ("custom", 1) + NAV_EFFICIENT = ("nav_efficient", 2) + AUTO_DEFLECTION = ("auto_deflection", 3) + + +class MowerBoundaryPerception(MowerEnum): + """Boundary perception preference (`MowPreference.BoundaryPerception`).""" + + UNKNOWN = ("unknown_bp", 0) + STANDARD = ("standard", 1) + INTELLIGENCE = ("intelligence", 2) + + +class MowerPreferenceMode(MowerEnum): + """Preference scope used by preferences, configs and plans.""" + + UNKNOWN = ("unknown", -1) + NONE = ("none", 0) + GLOBAL = ("global", 1) + CUSTOM = ("custom", 2) + + +class MowerPlanType(MowerEnum): + """Schedule plan recurrence (`MowingPlan.Type`).""" + + UNKNOWN = ("unknown", -1) + NONE = ("none", 0) + ONE_TIME = ("one_time", 1) + WEEKLY = ("weekly", 2) + + +class MowerPlanStatus(MowerEnum): + """Schedule plan status (`MowingPlan.Status`).""" + + UNKNOWN = ("unknown", 0) + OPEN = ("open", 1) + CLOSE = ("close", 2) + INVALID = ("invalid", 3) + + +class MowerWeekday(MowerEnum): + """Weekday for weekly plans (`rock.common.Week.Type`), Sunday first.""" + + UNKNOWN = ("unknown", -1) + SUNDAY = ("sunday", 0) + MONDAY = ("monday", 1) + TUESDAY = ("tuesday", 2) + WEDNESDAY = ("wednesday", 3) + THURSDAY = ("thursday", 4) + FRIDAY = ("friday", 5) + SATURDAY = ("saturday", 6) + + +class MowerRainType(MowerEnum): + """Reported rain intensity (`rock.common.RainFall.Type`).""" + + UNKNOWN = ("unknown", 0) + RAIN_NO = ("rain_no", 1) + RAIN_LOW = ("rain_low", 2) + RAIN_MID = ("rain_mid", 3) + RAIN_HIGH = ("rain_high", 4) + + +class MowerNavSolution(MowerEnum): + """Positioning hardware (`rock.common.NavFeature.Solution`).""" + + UNKNOWN = ("unknown_solution", 0) + RTK_VISION = ("rtk_vision", 1) + LIDAR_VISION = ("lidar_vision", 2) + + +class MowerDriveMode(MowerEnum): + """Drive train (`rock.common.DriveFeature.Mode`).""" + + UNKNOWN = ("unknown_mode", 0) + TWO_WHEEL_DRIVE = ("two_wheel_drive", 1) + FOUR_WHEEL_DRIVE = ("four_wheel_drive", 2) + + +class MowerMobile4GMode(MowerEnum): + """Cellular hardware (`rock.common.Mobile4GFeature.Mode`).""" + + UNKNOWN = ("unknown_mode", 0) + INTERNAL_ESIM = ("internal_esim", 1) + EXTERNAL_ESIM = ("external_esim", 2) + + +class MowerSkuRegion(MowerEnum): + """Market region (`rock.common.SkuInfo.Region`).""" + + UNKNOWN = ("unknown_region", 0) + US = ("us", 1) + EU = ("eu", 2) + + +class MowerStartDpType(MowerEnum): + """Structured start request type (`rock.iot.StartDpValue.Type`).""" + + UNKNOWN = ("unknown", 0) + GLOBAL = ("global", 1) + SELECTION_WITH_ID = ("selection_with_id", 2) + EDGE_WITH_ID = ("edge_with_id", 3) + ZONING_WITH_PARAMS = ("zoning_with_params", 4) + + +class MowerFeature(MowerEnum): + """Capability bit positions in `FeatureSet.details` (`rock.common.FeatureSet.Type`).""" + + UNKNOWN = ("unknown", -1) + RTK = ("rtk", 0) + LIDAR = ("lidar", 1) + HEIGHT_MOTOR = ("height_motor", 2) + FOUR_WHEELS_DRIVE = ("four_wheels_drive", 3) + SIGN_LED = ("sign_led", 4) + FEATURESET_NEW = ("featureset_new", 6) + LTE_LOG = ("lte_log", 7) + AFS_LINE_DIFF_CALIBRATION = ("afs_line_diff_calibration", 8) + PREFERENCE_AUTO_DEFLECTION = ("preference_auto_deflection", 9) + EXHIBITION = ("exhibition", 10) + MAP_RENAME = ("map_rename", 11) + LORA_LOGIC_OPTIMIZATION = ("lora_logic_optimization", 12) + RESET_UNDOCK = ("reset_undock", 13) + SUPPORT_SIM = ("support_sim", 14) + REMOTE_EDGE_CUTTER = ("remote_edge_cutter", 15) + NO_AVOID = ("no_avoid", 16) + RGB_MOWER = ("rgb_mower", 17) + GRASS_ROPE = ("grass_rope", 18) + NRTK_MODE = ("nrtk_mode", 19) + DOCK_NRTK_MODE = ("dock_nrtk_mode", 20) + DOCK_WIFI = ("dock_wifi", 21) + REALTIME_VIDEO = ("realtime_video", 22) + LIVING_OBSTACLE = ("living_obstacle", 23) + EDGE_VISION_AVOID = ("edge_vision_avoid", 24) + DOCK_WIFI_LORA = ("dock_wifi_lora", 25) + OBS_IMAGE = ("obs_image", 26) + PANORAMA = ("panorama", 27) + LIFT_AND_SAFETY_LOCK = ("lift_and_safety_lock", 28) + CHILD_LOCKED = ("child_locked", 29) + RESET_PASSAGE = ("reset_passage", 30) + REMOTE_DOUBLE_AXIS_JOYSTICK = ("remote_double_axis_joystick", 31) + BOUNDARY_TYPE = ("boundary_type", 32) + S1G = ("s1g", 33) + EDIT_BOUNDARY = ("edit_boundary", 34) + NETWORK_INFO_V1 = ("network_info_v1", 35) + STATIC_OBSTACLE = ("static_obstacle", 36) + + +class MowerMcuType(MowerEnum): + """Controller identity in MCU version reports (`rock.common.McuType.Type`).""" + + UNKNOWN = ("unknown", -1) + MAIN_MCU = ("main_mcu", 0) + REAR_DRV_MCU = ("rear_drv_mcu", 1) + BORDER_DRV_MCU = ("border_drv_mcu", 2) + MOVE_STATION_MCU = ("move_station_mcu", 4) + MOVE_STATION_RTK = ("move_station_rtk", 5) + MOVE_STATION_LORA = ("move_station_lora", 6) + BASE_STATION_MCU = ("base_station_mcu", 7) + BASE_STATION_RTK = ("base_station_rtk", 8) + BASE_STATION_LORA = ("base_station_lora", 9) + FRONT_DRV_MCU = ("front_drv_mcu", 10) + FRONT_STEER_MCU = ("front_steer_mcu", 11) + FRONT_LEFT_DRIVE_MCU = ("front_left_drive_mcu", 14) + FRONT_LEFT_STEER_MCU = ("front_left_steer_mcu", 15) + FRONT_RIGHT_DRIVE_MCU = ("front_right_drive_mcu", 16) + FRONT_RIGHT_STEER_MCU = ("front_right_steer_mcu", 17) + REAR_LEFT_DRIVE_MCU = ("rear_left_drive_mcu", 18) + REAR_RIGHT_DRIVE_MCU = ("rear_right_drive_mcu", 19) + TRIM0_MCU = ("trim0_mcu", 20) + TRIM1_MCU = ("trim1_mcu", 21) + + +class MowerRtkMode(MowerEnum): + """RTK correction mode (`rock.common.RtkMode.Mode`).""" + + UNKNOWN = ("unknown", -1) + DEFAULT = ("default", 0) + BASE_RTK = ("base_rtk", 1) + BASE_NRTK = ("base_nrtk", 2) + DOCK_NRTK = ("dock_nrtk", 3) + AUTO_NRTK = ("auto_nrtk", 4) diff --git a/roborock/data/mower/mower_commands.py b/roborock/data/mower/mower_commands.py new file mode 100644 index 000000000..089080784 --- /dev/null +++ b/roborock/data/mower/mower_commands.py @@ -0,0 +1,489 @@ +"""Typed request and reply payloads for the mower remote_pb protocol. + +Fields may be absent from partial replies. ``as_dict()`` uses the normal +RoborockBase representation; it is not a remote_pb wire serializer. +An absent scalar or enum field stays ``None`` here, even when protobuf may +have omitted its default value. This records payload presence, not whether a setting is unset; +the protocol/consumer layer must interpret defaults for each message type. +""" + +from dataclasses import dataclass +from typing import Any, ClassVar + +from roborock.data.containers import RoborockBase + +from .mower_code_mappings import ( + MowerAckResult, + MowerAppButton, + MowerBoundaryPerception, + MowerDetailState, + MowerDirectionType, + MowerDriveMode, + MowerFsmEvent, + MowerFsmState, + MowerMcuType, + MowerMobile4GMode, + MowerNavSolution, + MowerPlanStatus, + MowerPlanType, + MowerPreferenceEffective, + MowerPreferenceMode, + MowerRainType, + MowerRemoteMsgType, + MowerRobotMsgType, + MowerRtkMode, + MowerSkuRegion, + MowerWeekday, +) + +__all__ = [ + "MowerAck", + "MowerPose", + "MowerBoundaryRef", + "MowerMapSelection", + "MowerTimePoint", + "MowerTimeSlot", + "MowerNotDisturbConfig", + "MowerRainfallConfig", + "MowerAudioConfig", + "MowerPreference", + "MowerPreferenceConfig", + "MowerPlanDay", + "MowerMowingPlan", + "MowerMowSchedule", + "MowerHeightMotorParameter", + "MowerRobotTask", + "MowerMowContinue", + "MowerFeatureSet", + "MowerMainCutterHeight", + "MowerSkuInfo", + "MowerCutterFeature", + "MowerNavFeature", + "MowerDriveFeature", + "MowerMobile4GFeature", + "MowerFeatureInfo", + "MowerMcuVersionItem", + "MowerMcuVersion", + "MowerBattery", + "MowerAppResponseError", + "MowerUserModeConfig", + "MowerRemoteMsg", + "MowerRobotMsg", + "MowerRobotInfo", + "MowerDeviceInfo", +] + + +@dataclass +class MowerAck(RoborockBase): + """Request acceptance (``rock.common.Ack``).""" + + result: MowerAckResult | None = None + id: str | None = None + """Echo of the 64-bit request id, carried as a decimal string like ``RemoteMsg.id``.""" + + +@dataclass +class MowerPose(RoborockBase): + """Local map pose (``rock.common.Pose``).""" + + x: float | None = None + y: float | None = None + yaw: float | None = None + + +@dataclass +class MowerBoundaryRef(RoborockBase): + """Reference to a saved boundary by ID, optionally by name on older firmware.""" + + id: int | None = None + name: str | None = None + + +@dataclass +class MowerMapSelection(RoborockBase): + """The subset of ``rock.common.Map`` carried in ``RemoteMsg.modify_map``. + + Selected mowing uses ``boundaries``; map file requests use ``name``. + """ + + name: str | None = None + boundaries: list[MowerBoundaryRef] | None = None + + +@dataclass +class MowerTimePoint(RoborockBase): + """Hour and minute of day (``rock.common.TimePoint``).""" + + hour: int | None = None + minute: int | None = None + + +@dataclass +class MowerTimeSlot(RoborockBase): + """Start and end time of day (``rock.common.TimeSlot``).""" + + start: MowerTimePoint | None = None + end: MowerTimePoint | None = None + + +@dataclass +class MowerNotDisturbConfig(RoborockBase): + """Animal-protection hours (``rock.common.NotDisturb``). + + Protected mower-local periods can cause a return to charge and later + continuation. + """ + + enable: bool | None = None + time: list[MowerTimeSlot] | None = None + + +@dataclass +class MowerRainfallConfig(RoborockBase): + """Rain protection (``rock.common.RainFall``). + + ``delay_time`` is the resume delay in hours after rain; the app offers 0, + 3 and 8. ``type`` is the reported rain intensity and is read-only. + """ + + type: MowerRainType | None = None + enable: bool | None = None + delay_time: float | None = None + + +@dataclass +class MowerAudioConfig(RoborockBase): + """Voice volume (``rock.common.AudioConfig``).""" + + volume: int | None = None + + +@dataclass +class MowerPreference(RoborockBase): + """Mowing preference for the whole lawn or one area (``MowPreference``).""" + + mow_times: int | None = None + effective: MowerPreferenceEffective | None = None + direction: int | None = None + """Mowing direction angle in degrees.""" + height: int | None = None + """Cutting height in device units, within the height motor limits.""" + keep_edge: int | None = None + """Boundary trimming passes: 0 none, 1 one pass, 2 two passes.""" + area_name: str | None = None + area_id: int | None = None + mode: MowerPreferenceMode | None = None + direction_type: MowerDirectionType | None = None + custom_zig_zag: int | None = None + custom_enable_cutter: bool | None = None + total_times: int | None = None + height_bound_trim: bool | None = None + boundary_perception: MowerBoundaryPerception | None = None + + +@dataclass +class MowerPreferenceConfig(RoborockBase): + """Global and per-area preferences (``MowAreasConfig``). + + The wire field ``global`` is a Python keyword, so it is exposed as + ``global_preference``; parsing accepts both names. ``as_dict`` keeps the + canonical field name. Protocol serialization must apply ``WIRE_ALIASES`` + and serialize enum names separately from these domain values. + """ + + WIRE_ALIASES: ClassVar[dict[str, str]] = {"global_preference": "global"} + """One-off field-to-wire-key rename applied by the protocol serializer.""" + + global_preference: MowerPreference | None = None + custom: list[MowerPreference] | None = None + mode: MowerPreferenceMode | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]): + """Create the config, accepting the ``global`` wire key.""" + if isinstance(data, dict) and "global" in data: + data = {("global_preference" if key == "global" else key): value for key, value in data.items()} + return super().from_dict(data) + + +@dataclass +class MowerPlanDay(RoborockBase): + """One weekday entry of a weekly plan (``rock.common.Week``).""" + + type: MowerWeekday | None = None + + +@dataclass +class MowerMowingPlan(RoborockBase): + """A scheduled mowing plan (``MowingPlan``). + + Times are Unix seconds in the mower's time zone. ``fsm_state`` selects the + scheduled task (``MOW_GLOBAL``, ``MOW_EDGE`` or ``MOW_SELECT``). + """ + + id: int | None = None + type: MowerPlanType | None = None + status: MowerPlanStatus | None = None + start: int | None = None + end: int | None = None + days: list[MowerPlanDay] | None = None + mode: MowerPreferenceMode | None = None + config: MowerPreferenceConfig | None = None + fsm_state: MowerFsmState | None = None + + +@dataclass +class MowerMowSchedule(RoborockBase): + """All plans stored on the mower (``MowSchedule``).""" + + plans: list[MowerMowingPlan] | None = None + time_zone: str | None = None + update_time: int | None = None + + +@dataclass +class MowerHeightMotorParameter(RoborockBase): + """Cutting height limits (``rock.common.HeightMotorParameter``). + + A valid height lies on a ``step`` multiple between ``min`` and ``max``. + """ + + max: int | None = None + min: int | None = None + step: int | None = None + + +@dataclass +class MowerRobotTask(RoborockBase): + """Current task state (``rock.common.RobotTask``).""" + + working_state: MowerFsmState | None = None + """The coarse task-machine state; the detailed state is separate.""" + robot_detail_state: MowerDetailState | None = None + + +@dataclass +class MowerMowContinue(RoborockBase): + """Retained-task continuation state (``rock.common.MowContinue``).""" + + continue_state: int | None = None + plan_mow: bool | None = None + mow_dock_manual: bool | None = None + plan_id: int | None = None + plan_start: int | None = None + plan_end: int | None = None + + +@dataclass +class MowerFeatureSet(RoborockBase): + """Capability words (``rock.common.FeatureSet``). + + Each entry is an unsigned 64-bit word; bit numbering follows + :class:`~roborock.data.mower.MowerFeature`. + """ + + details: list[int] | None = None + + +@dataclass +class MowerMainCutterHeight(RoborockBase): + """SKU cutting height data (``rock.common.MainCutterHeight``).""" + + default_height: int | None = None + calibrate_height: int | None = None + min_height: int | None = None + max_height: int | None = None + step_height: int | None = None + + +@dataclass +class MowerSkuInfo(RoborockBase): + """Market SKU data (``rock.common.SkuInfo``).""" + + market_name: str | None = None + declare_cut_area: int | None = None + charge_current: int | None = None + battery_capacity: float | None = None + region: MowerSkuRegion | None = None + real_cut_area: int | None = None + main_cutter_height: MowerMainCutterHeight | None = None + + +@dataclass +class MowerCutterFeature(RoborockBase): + """Cutter hardware (``rock.common.CutterFeature``).""" + + main_cutter_radius: float | None = None + main_cutter_diameter_mm: int | None = None + + +@dataclass +class MowerNavFeature(RoborockBase): + """Positioning hardware (``rock.common.NavFeature``).""" + + solution: MowerNavSolution | None = None + + +@dataclass +class MowerDriveFeature(RoborockBase): + """Drive train (``rock.common.DriveFeature``).""" + + mode: MowerDriveMode | None = None + + +@dataclass +class MowerMobile4GFeature(RoborockBase): + """Cellular hardware (``rock.common.Mobile4GFeature``).""" + + mode: MowerMobile4GMode | None = None + + +@dataclass +class MowerFeatureInfo(RoborockBase): + """Hardware descriptors (``rock.common.FeatureInfo``).""" + + cutter_feature: MowerCutterFeature | None = None + nav_feature: MowerNavFeature | None = None + drive_feature: MowerDriveFeature | None = None + mobile_4g_feature: MowerMobile4GFeature | None = None + sku_info: MowerSkuInfo | None = None + + +@dataclass +class MowerMcuVersionItem(RoborockBase): + """One controller firmware version (``rock.common.McuVersionItem``).""" + + mcu_type: MowerMcuType | None = None + version: str | None = None + + +@dataclass +class MowerMcuVersion(RoborockBase): + """Controller firmware versions (``rock.common.McuVersion``).""" + + project: int | None = None + mcu_versions: list[MowerMcuVersionItem] | None = None + + +@dataclass +class MowerBattery(RoborockBase): + """Battery details (``rock.common.Battery``).""" + + charged: bool | None = None + percent: int | None = None + voltage: int | None = None + current: int | None = None + + +@dataclass +class MowerAppResponseError(RoborockBase): + """Error returned for a rejected request (``rock.common.AppResponseError``).""" + + error_code: int | None = None + error_msg: str | None = None + + +@dataclass +class MowerUserModeConfig(RoborockBase): + """User settings reported by ``GET_USER_MODE_CONFIG`` (``rock.common.UserModeConfig``). + + Only the consumer-facing settings are modelled; installation, network, + anti-theft and video configuration are not yet exposed. + """ + + rainfall_config: MowerRainfallConfig | None = None + not_disturb_config: MowerNotDisturbConfig | None = None + audio_config: MowerAudioConfig | None = None + rtk_mode_config: MowerRtkMode | None = None + lift_alarm_enable: bool | None = None + child_lock_enable: bool | None = None + + +@dataclass +class MowerRemoteMsg(RoborockBase): + """A structured command (``rock.common.remote.RemoteMsg``). + + ``id`` is a 64-bit request identifier carried as a decimal string on the + JSON route. Only the payload fields with dedicated support are modelled. + """ + + id: str | None = None + type: MowerRemoteMsgType | None = None + app_button: MowerAppButton | None = None + modify_map: MowerMapSelection | None = None + target: MowerPose | None = None + start_index: int | None = None + play_audio: int | None = None + record_time_stamp: int | None = None + mow_preference: MowerPreference | None = None + mow_preference_config: MowerPreferenceConfig | None = None + not_disturb_config: MowerNotDisturbConfig | None = None + rainfall_config: MowerRainfallConfig | None = None + audio_config: MowerAudioConfig | None = None + mowing_plan: MowerMowingPlan | None = None + region_ids: list[int] | None = None + new_task: bool | None = None + clear_user_code: int | None = None + cache_fsm_event: int | None = None + button_with_map_selfcheck_result: MowerFsmEvent | None = None + lift_alarm_enable: bool | None = None + child_lock_enable: bool | None = None + + +@dataclass +class MowerRobotMsg(RoborockBase): + """A structured reply or event (``rock.common.RobotMsg``). + + Only the fields consumed by the library are modelled; a reply carries the + subset relevant to its ``type``. + """ + + id: str | None = None + type: MowerRobotMsgType | None = None + ack: MowerAck | None = None + app_response_error: MowerAppResponseError | None = None + map_names: list[str] | None = None + battery: MowerBattery | None = None + preference_config: MowerPreferenceConfig | None = None + user_mode_config: MowerUserModeConfig | None = None + features: MowerFeatureSet | None = None + height_motor_parameter: MowerHeightMotorParameter | None = None + mow_schedule: MowerMowSchedule | None = None + next_plan: MowerMowingPlan | None = None + time_zone: str | None = None + mow_working_state: MowerFsmState | None = None + sku_info: MowerSkuInfo | None = None + sw_version: str | None = None + mcu_version: MowerMcuVersion | None = None + project: int | None = None + robot_task: MowerRobotTask | None = None + mow_continue: MowerMowContinue | None = None + feature_info: MowerFeatureInfo | None = None + serial_number: str | None = None + + +@dataclass +class MowerRobotInfo(RoborockBase): + """Task and battery state read with ``GET_ROBOT_INFO``.""" + + robot_task: MowerRobotTask | None = None + battery: MowerBattery | None = None + mow_continue: MowerMowContinue | None = None + mow_working_state: MowerFsmState | None = None + + @property + def detail_state(self) -> MowerDetailState | None: + """The detailed task state, if reported.""" + return self.robot_task.robot_detail_state if self.robot_task is not None else None + + +@dataclass +class MowerDeviceInfo(RoborockBase): + """Identity and version information read from the mower.""" + + sw_version: str | None = None + mcu_version: MowerMcuVersion | None = None + serial_number: str | None = None + sku_info: MowerSkuInfo | None = None + time_zone: str | None = None diff --git a/tests/data/mower/test_mower_code_mappings.py b/tests/data/mower/test_mower_code_mappings.py index f32a1c5ed..cd54fe7a4 100644 --- a/tests/data/mower/test_mower_code_mappings.py +++ b/tests/data/mower/test_mower_code_mappings.py @@ -7,17 +7,23 @@ from roborock.data import mower from roborock.data.code_mappings import RoborockModeEnum from roborock.data.mower import ( + MowerAckResult, + MowerAppButton, MowerChargeState, MowerDetailState, MowerEffMode, MowerEnum, MowerErrorCode, MowerFcState, + MowerFeature, MowerMowState, MowerMowType, MowerNetworkChannel, MowerOfflineStatus, MowerPendType, + MowerRemoteMsgType, + MowerRobotMsgType, + MowerWeekday, ) ALL_MOWER_ENUMS: list[type[RoborockModeEnum]] = sorted( @@ -76,6 +82,15 @@ def test_mower_enum_codes_are_unique(enum_cls: type[RoborockModeEnum]) -> None: (MowerErrorCode, 48, MowerErrorCode.CAMERA_REAR_SIDE_DIRTY), (MowerErrorCode, 55, MowerErrorCode.N_RTK_NETWORK_ERROR), (MowerErrorCode, 118, MowerErrorCode.REAR_SIDE_CAMERA_FAULT), + # Separate request/reply domains and boundary values in the app schema. + (MowerRemoteMsgType, 6, MowerRemoteMsgType.APP_BUTTON), + (MowerRobotMsgType, 6, MowerRobotMsgType.SELF_CHECK_STATUS), + (MowerRemoteMsgType, 166, MowerRemoteMsgType.SAVE_S1G_IOT_CONFIG), + (MowerRobotMsgType, 122, MowerRobotMsgType.S1G_PAIR_STATUS), + (MowerAppButton, 16, MowerAppButton.MOW_SELECT), + (MowerAckResult, 0, MowerAckResult.OK), + (MowerWeekday, 0, MowerWeekday.SUNDAY), + (MowerFeature, 0, MowerFeature.RTK), ], ) def test_mower_enum_code_lookup(enum_cls: type[RoborockModeEnum], code: int, expected: RoborockModeEnum) -> None: @@ -176,3 +191,12 @@ def test_optional_mower_enum_lookups_remain_silent(caplog: pytest.LogCaptureFixt assert MowerErrorCode.from_any_optional("FUTURE_VALUE") is None assert not caplog.records + + +@pytest.mark.parametrize("enum_cls", ALL_MOWER_ENUMS, ids=lambda cls: cls.__name__) +def test_unknown_firmware_value_has_a_fallback(enum_cls: type[MowerEnum]) -> None: + """Future names and codes must not drop typed command fields or raise.""" + expected = enum_cls.from_name("UNKNOWN") + assert enum_cls.from_code(999999) is expected + assert enum_cls.from_code("FUTURE_FIRMWARE_VALUE") is expected + assert enum_cls.from_code_optional(999999) is None diff --git a/tests/data/mower/test_mower_commands.py b/tests/data/mower/test_mower_commands.py new file mode 100644 index 000000000..baf93cf13 --- /dev/null +++ b/tests/data/mower/test_mower_commands.py @@ -0,0 +1,347 @@ +"""Tests for typed mower command and reply payloads.""" + +from roborock.data import ( + MowerAck, + MowerAckResult, + MowerAppButton, + MowerAppResponseError, + MowerAudioConfig, + MowerBattery, + MowerBoundaryRef, + MowerCutterFeature, + MowerDetailState, + MowerDirectionType, + MowerDriveFeature, + MowerDriveMode, + MowerFeatureSet, + MowerFsmState, + MowerMainCutterHeight, + MowerMapSelection, + MowerMcuType, + MowerMcuVersion, + MowerMcuVersionItem, + MowerMobile4GFeature, + MowerMobile4GMode, + MowerMowContinue, + MowerMowingPlan, + MowerNavFeature, + MowerNavSolution, + MowerNotDisturbConfig, + MowerPlanDay, + MowerPlanStatus, + MowerPlanType, + MowerPreference, + MowerPreferenceConfig, + MowerPreferenceEffective, + MowerPreferenceMode, + MowerRainfallConfig, + MowerRainType, + MowerRemoteMsg, + MowerRemoteMsgType, + MowerRobotInfo, + MowerRobotMsg, + MowerRobotMsgType, + MowerRobotTask, + MowerRtkMode, + MowerSkuInfo, + MowerSkuRegion, + MowerTimePoint, + MowerTimeSlot, + MowerWeekday, +) + +CAPTURED_PREFERENCE_REPLY = { + "type": "MOW_PREFERENCE_CONFIG", + "preference_config": { + "global": { + "mow_times": 1, + "effective": "DAILY", + "direction": 5, + "keep_edge": 1, + "mode": "GLOBAL", + "direction_type": "AUTO_DEFLECTION", + }, + "custom": [ + {"effective": "MANICURE", "area_name": "A1", "area_id": 2, "mode": "CUSTOM"}, + {"effective": "MANICURE", "area_name": "A2", "area_id": 3, "mode": "CUSTOM"}, + {"effective": "DAILY", "area_name": "A3", "area_id": 4, "mode": "GLOBAL"}, + ], + "mode": "GLOBAL", + }, +} + + +def test_robot_msg_from_captured_preference_reply() -> None: + reply = MowerRobotMsg.from_dict(CAPTURED_PREFERENCE_REPLY) + + assert reply.type is MowerRobotMsgType.MOW_PREFERENCE_CONFIG + config = reply.preference_config + assert config is not None + assert config.mode is MowerPreferenceMode.GLOBAL + assert config.global_preference == MowerPreference( + mow_times=1, + effective=MowerPreferenceEffective.DAILY, + direction=5, + keep_edge=1, + mode=MowerPreferenceMode.GLOBAL, + direction_type=MowerDirectionType.AUTO_DEFLECTION, + ) + assert config.custom == [ + MowerPreference( + effective=MowerPreferenceEffective.MANICURE, + area_name="A1", + area_id=2, + mode=MowerPreferenceMode.CUSTOM, + ), + MowerPreference( + effective=MowerPreferenceEffective.MANICURE, + area_name="A2", + area_id=3, + mode=MowerPreferenceMode.CUSTOM, + ), + MowerPreference( + effective=MowerPreferenceEffective.DAILY, + area_name="A3", + area_id=4, + mode=MowerPreferenceMode.GLOBAL, + ), + ] + # Omitted fields stay None rather than defaulting to zero. + assert config.global_preference.height is None + assert config.custom[0].mow_times is None + + +def test_preference_config_round_trips_through_as_dict() -> None: + config = MowerRobotMsg.from_dict(CAPTURED_PREFERENCE_REPLY).preference_config + assert config is not None + + assert MowerPreferenceConfig.from_dict(config.as_dict()) == config + + +def test_remote_msg_from_documented_select_example() -> None: + """The documented MOW_SELECT request parses into a typed message.""" + msg = MowerRemoteMsg.from_dict( + { + "id": "1793289600002", + "type": "APP_BUTTON", + "app_button": "MOW_SELECT", + "modify_map": {"boundaries": [{"id": 7}, {"id": 12}]}, + } + ) + + assert msg == MowerRemoteMsg( + id="1793289600002", + type=MowerRemoteMsgType.APP_BUTTON, + app_button=MowerAppButton.MOW_SELECT, + modify_map=MowerMapSelection(boundaries=[MowerBoundaryRef(id=7), MowerBoundaryRef(id=12)]), + ) + + +def test_remote_msg_accepts_integer_enum_codes() -> None: + """Enum fields accept the wire integer as well as the protobuf name.""" + msg = MowerRemoteMsg.from_dict({"id": 42, "type": 6, "app_button": 14}) + + assert msg.id == "42" + assert msg.type is MowerRemoteMsgType.APP_BUTTON + assert msg.app_button is MowerAppButton.MOW_GLOBAL + + +def test_robot_msg_robot_info_reply() -> None: + reply = MowerRobotMsg.from_dict( + { + "type": "ROBOT_INFO", + "robot_task": {"working_state": 3, "robot_detail_state": "MOW_ZIG_ZAG"}, + "mow_continue": {"continue_state": 1, "plan_mow": True, "plan_id": 7}, + "battery": {"charged": False, "percent": 64, "voltage": 20100, "current": 0}, + "mow_working_state": 18, + } + ) + + assert reply.type is MowerRobotMsgType.ROBOT_INFO + assert reply.robot_task == MowerRobotTask( + working_state=MowerFsmState.SUSPEND, robot_detail_state=MowerDetailState.MOW_ZIG_ZAG + ) + assert reply.mow_continue == MowerMowContinue(continue_state=1, plan_mow=True, plan_id=7) + assert reply.battery == MowerBattery(charged=False, percent=64, voltage=20100, current=0) + + +def test_robot_msg_feature_set_accepts_decimal_strings() -> None: + """64-bit words may be serialized as decimal strings on the JSON route.""" + reply = MowerRobotMsg.from_dict({"type": "FEATURESET", "features": {"details": ["18446744073709551615", 5]}}) + + assert reply.features == MowerFeatureSet(details=[2**64 - 1, 5]) + + +def test_robot_msg_hardware_and_version_replies() -> None: + reply = MowerRobotMsg.from_dict( + { + "type": "FEATURE_INFO", + "feature_info": { + "cutter_feature": {"main_cutter_radius": 0.1, "main_cutter_diameter_mm": 200}, + "nav_feature": {"solution": "RTK_VISION"}, + "drive_feature": {"mode": 2}, + "mobile_4g_feature": {"mode": "INTERNAL_ESIM"}, + "sku_info": { + "market_name": "RockNeo Q1", + "declare_cut_area": 1000, + "battery_capacity": 5.2, + "region": "EU", + "main_cutter_height": {"min_height": 30, "max_height": 70, "step_height": 5}, + }, + }, + "mcu_version": {"project": 1, "mcu_versions": [{"mcu_type": 1, "version": "1.2.3"}]}, + "sw_version": "02.14.06", + "serial_number": "SN123", + "time_zone": "Europe/Copenhagen", + } + ) + + info = reply.feature_info + assert info is not None + assert info.cutter_feature == MowerCutterFeature(main_cutter_radius=0.1, main_cutter_diameter_mm=200) + assert info.nav_feature == MowerNavFeature(solution=MowerNavSolution.RTK_VISION) + assert info.drive_feature == MowerDriveFeature(mode=MowerDriveMode.FOUR_WHEEL_DRIVE) + assert info.mobile_4g_feature == MowerMobile4GFeature(mode=MowerMobile4GMode.INTERNAL_ESIM) + assert info.sku_info == MowerSkuInfo( + market_name="RockNeo Q1", + declare_cut_area=1000, + battery_capacity=5.2, + region=MowerSkuRegion.EU, + main_cutter_height=MowerMainCutterHeight(min_height=30, max_height=70, step_height=5), + ) + assert reply.mcu_version == MowerMcuVersion( + project=1, mcu_versions=[MowerMcuVersionItem(mcu_type=MowerMcuType.REAR_DRV_MCU, version="1.2.3")] + ) + assert reply.sw_version == "02.14.06" + assert reply.serial_number == "SN123" + assert reply.time_zone == "Europe/Copenhagen" + + +def test_robot_msg_user_mode_config_and_schedule() -> None: + reply = MowerRobotMsg.from_dict( + { + "type": "USER_MODE_CONFIG", + "user_mode_config": { + "rainfall_config": {"type": "RAIN_NO", "enable": True, "delay_time": 3.0}, + "not_disturb_config": { + "enable": True, + "time": [{"start": {"hour": 22, "minute": 0}, "end": {"hour": 6, "minute": 30}}], + }, + "audio_config": {"volume": 60}, + "rtk_mode_config": 1, + "child_lock_enable": False, + }, + "mow_schedule": { + "time_zone": "Europe/Copenhagen", + "update_time": 1700000000, + "plans": [ + { + "id": 12345, + "type": "WEEKLY", + "status": "OPEN", + "start": 1700000000, + "end": 1700007200, + "days": [{"type": "MONDAY"}, {"type": 4}], + "mode": "GLOBAL", + "fsm_state": "MOW_GLOBAL", + "config": {"global": {"mow_times": 2}, "mode": "GLOBAL"}, + } + ], + }, + } + ) + + config = reply.user_mode_config + assert config is not None + assert config.rainfall_config == MowerRainfallConfig(type=MowerRainType.RAIN_NO, enable=True, delay_time=3.0) + assert config.not_disturb_config == MowerNotDisturbConfig( + enable=True, + time=[MowerTimeSlot(start=MowerTimePoint(hour=22, minute=0), end=MowerTimePoint(hour=6, minute=30))], + ) + assert config.audio_config == MowerAudioConfig(volume=60) + assert config.rtk_mode_config is MowerRtkMode.BASE_RTK + assert config.child_lock_enable is False + + schedule = reply.mow_schedule + assert schedule is not None + assert schedule.time_zone == "Europe/Copenhagen" + assert schedule.plans == [ + MowerMowingPlan( + id=12345, + type=MowerPlanType.WEEKLY, + status=MowerPlanStatus.OPEN, + start=1700000000, + end=1700007200, + days=[MowerPlanDay(type=MowerWeekday.MONDAY), MowerPlanDay(type=MowerWeekday.THURSDAY)], + mode=MowerPreferenceMode.GLOBAL, + fsm_state=MowerFsmState.MOW_GLOBAL, + config=MowerPreferenceConfig( + global_preference=MowerPreference(mow_times=2), + mode=MowerPreferenceMode.GLOBAL, + ), + ) + ] + + +def test_robot_msg_ack_and_error() -> None: + accepted = MowerRobotMsg.from_dict({"type": "ACK", "ack": {"result": "OK", "id": 1793289600001}}) + rejected = MowerRobotMsg.from_dict( + {"type": "APP_RESPONSE_ERROR", "app_response_error": {"error_code": 7, "error_msg": "busy"}} + ) + + assert accepted.ack == MowerAck(result=MowerAckResult.OK, id="1793289600001") + assert rejected.app_response_error == MowerAppResponseError(error_code=7, error_msg="busy") + + +def test_robot_info_detail_state_property() -> None: + assert MowerRobotInfo().detail_state is None + assert ( + MowerRobotInfo(robot_task=MowerRobotTask(robot_detail_state=MowerDetailState.FREE)).detail_state + is MowerDetailState.FREE + ) + + +def test_robot_msg_unknown_enum_values_resolve_to_unknown() -> None: + """Values from newer firmware resolve to the explicit unknown member.""" + reply = MowerRobotMsg.from_dict( + { + "type": "SOME_FUTURE_TYPE", + "robot_task": {"working_state": 999, "robot_detail_state": 999}, + "user_mode_config": {"rtk_mode_config": 999}, + } + ) + + assert reply.type is MowerRobotMsgType.UNKNOWN + assert reply.robot_task == MowerRobotTask( + working_state=MowerFsmState.UNKNOWN, robot_detail_state=MowerDetailState.UNKNOWN + ) + assert reply.user_mode_config is not None + assert reply.user_mode_config.rtk_mode_config is MowerRtkMode.UNKNOWN + + +def test_partial_command_preserves_zero_false_empty_and_large_id() -> None: + """Omitted fields stay None, while supplied zero/false/empty values survive.""" + msg = MowerRemoteMsg.from_dict( + {"id": "18446744073709551615", "type": "APP_BUTTON", "start_index": 0, "new_task": False, "region_ids": []} + ) + assert msg.id == "18446744073709551615" + assert msg.start_index == 0 + assert msg.new_task is False + assert msg.region_ids == [] + assert msg.app_button is None + assert msg.as_dict() == { + "id": "18446744073709551615", + "type": "app_button", + "startIndex": 0, + "newTask": False, + "regionIds": [], + } + assert MowerRemoteMsg.from_dict(msg.as_dict()) == msg + + +def test_omitted_zero_values_retain_payload_presence() -> None: + """The model distinguishes omitted scalars from explicitly supplied wire zeroes.""" + assert MowerAck.from_dict({"id": "5"}) == MowerAck(id="5") + assert MowerAck.from_dict({"id": "5", "result": 0}) == MowerAck(id="5", result=MowerAckResult.OK) + assert MowerPlanDay.from_dict({}) == MowerPlanDay() + assert MowerPlanDay.from_dict({"type": 0}) == MowerPlanDay(type=MowerWeekday.SUNDAY)