diff --git a/roborock/data/__init__.py b/roborock/data/__init__.py index a3e7863a..121f8b3c 100644 --- a/roborock/data/__init__.py +++ b/roborock/data/__init__.py @@ -5,5 +5,6 @@ from .code_mappings import * from .containers import * from .dyad import * +from .mower import * from .v1 import * from .zeo import * diff --git a/roborock/data/mower/__init__.py b/roborock/data/mower/__init__.py new file mode 100644 index 00000000..a12cd9e4 --- /dev/null +++ b/roborock/data/mower/__init__.py @@ -0,0 +1,3 @@ +"""Dataclasses for Roborock mower devices.""" + +from .mower_containers import * diff --git a/roborock/data/mower/mower_containers.py b/roborock/data/mower/mower_containers.py new file mode 100644 index 00000000..949bfbab --- /dev/null +++ b/roborock/data/mower/mower_containers.py @@ -0,0 +1,43 @@ +"""Data containers for Roborock mower devices.""" + +from dataclasses import dataclass, field +from typing import Any + +from roborock.data.containers import RoborockBase +from roborock.roborock_message import RoborockMowerDataProtocol + + +@dataclass +class MowerStatus(RoborockBase): + """Core mower status backed by mower DPS updates.""" + + error_code: int | 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: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_STATE}) + 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}) + mow_height: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_HEIGHT}) + 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}) + mow_progress: int | None = field(default=None, metadata={"dps": RoborockMowerDataProtocol.MOW_PROGRESS}) + 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( + 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}) diff --git a/roborock/roborock_message.py b/roborock/roborock_message.py index ae669670..72564b38 100644 --- a/roborock/roborock_message.py +++ b/roborock/roborock_message.py @@ -40,6 +40,45 @@ def _missing_(cls: type[Self], key) -> Self: raise ValueError(f"{key} not a valid key for Data Protocol") +class RoborockMowerDataProtocol(RoborockEnum): + UNKNOWN = 0 + ERROR_CODE = 120 + BATTERY = 121 + MOW_TYPE = 122 + MOW_STATE = 123 + MAPPING_TYPE = 124 + MAPPING_STATE = 125 + OTA_STATE = 126 + CHARGE_STATE = 127 + DOCK_STATE = 128 + CHARGE_TYPE = 129 + PEND_TYPE = 130 + REMOTE_STATE = 131 + MOW_START_TYPE = 132 + MOW_EFF_MODE = 133 + MOW_HEIGHT = 134 + MOW_DIRECTION_ANGLE = 135 + MOW_PATTERN = 136 + MOW_CONF_MODE = 137 + OFFLINE_STATUS = 138 + MOW_PROGRESS = 139 + BLADE_LIFESPAN = 140 + FC_STATE = 141 + GPS_COORDINATE = 142 + OFF_DOCK_NO_TASK_STATUS = 143 + AFS_STATUS = 144 + NETWORK_CHANNEL = 145 + START = 201 + DOCK = 202 + PAUSE = 203 + RESUME = 204 + STOP = 205 + + @classmethod + def _missing_(cls: type[Self], key) -> Self: + raise ValueError(f"{key} not a valid key for Mower Data Protocol") + + class RoborockDyadDataProtocol(RoborockEnum): DRYING_STATUS = 134 START = 200 diff --git a/tests/data/test_mower_containers.py b/tests/data/test_mower_containers.py new file mode 100644 index 00000000..6495ef2d --- /dev/null +++ b/tests/data/test_mower_containers.py @@ -0,0 +1,74 @@ +"""Test mower data containers.""" + +import dataclasses + +from roborock.data import MowerStatus +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 == 3 + assert status.mow_height == 70 + assert status.gps_coordinate == {"latitude": 1, "longitude": 2} + + +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