-
Notifications
You must be signed in to change notification settings - Fork 64
feat: Add initial Q10 support for Status Trait #769
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2462505
e93d35b
6eedd95
91dbcfb
356f31b
7601c6f
d8c0f7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,53 +1,102 @@ | ||
| from ..containers import RoborockBase | ||
| """Data container classes for Q10 B01 devices. | ||
| Many of these classes use the `field(metadata={"dps": ...})` convention to map | ||
| dataclass fields to device Data Points (DPS). This metadata is utilized by the | ||
| `update_from_dps` helper in `roborock.devices.traits.b01.q10.common` to | ||
| automatically update objects from raw device responses. | ||
| """ | ||
|
|
||
| from dataclasses import dataclass, field | ||
|
|
||
| from ..containers import RoborockBase | ||
| from .b01_q10_code_mappings import ( | ||
| B01_Q10_DP, | ||
| YXBackType, | ||
| YXDeviceCleanTask, | ||
| YXDeviceState, | ||
| YXDeviceWorkMode, | ||
| YXFanLevel, | ||
| YXWaterLevel, | ||
| ) | ||
|
|
||
|
|
||
| @dataclass | ||
| class dpCleanRecord(RoborockBase): | ||
| op: str | ||
| result: int | ||
| id: str | ||
| data: list | ||
|
|
||
|
|
||
| @dataclass | ||
| class dpMultiMap(RoborockBase): | ||
| op: str | ||
| result: int | ||
| data: list | ||
|
|
||
|
|
||
| @dataclass | ||
| class dpGetCarpet(RoborockBase): | ||
| op: str | ||
| result: int | ||
| data: str | ||
|
|
||
|
|
||
| @dataclass | ||
| class dpSelfIdentifyingCarpet(RoborockBase): | ||
| op: str | ||
| result: int | ||
| data: str | ||
|
|
||
|
|
||
| @dataclass | ||
| class dpNetInfo(RoborockBase): | ||
| wifiName: str | ||
| ipAdress: str | ||
| mac: str | ||
| signal: int | ||
|
|
||
|
|
||
| @dataclass | ||
| class dpNotDisturbExpand(RoborockBase): | ||
| disturb_dust_enable: int | ||
| disturb_light: int | ||
| disturb_resume_clean: int | ||
| disturb_voice: int | ||
|
|
||
|
|
||
| @dataclass | ||
| class dpCurrentCleanRoomIds(RoborockBase): | ||
| room_id_list: list | ||
|
|
||
|
|
||
| @dataclass | ||
| class dpVoiceVersion(RoborockBase): | ||
| version: int | ||
|
|
||
|
|
||
| @dataclass | ||
| class dpTimeZone(RoborockBase): | ||
| timeZoneCity: str | ||
| timeZoneSec: int | ||
|
|
||
|
|
||
| @dataclass | ||
| class Q10Status(RoborockBase): | ||
| """Status for Q10 devices. | ||
| Fields are mapped to DPS values using metadata. Objects of this class can be | ||
| automatically updated using the `update_from_dps` helper. | ||
| """ | ||
|
|
||
| clean_time: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TIME}) | ||
| clean_area: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_AREA}) | ||
| battery: int | None = field(default=None, metadata={"dps": B01_Q10_DP.BATTERY}) | ||
| status: YXDeviceState | None = field(default=None, metadata={"dps": B01_Q10_DP.STATUS}) | ||
| fan_level: YXFanLevel | None = field(default=None, metadata={"dps": B01_Q10_DP.FAN_LEVEL}) | ||
| water_level: YXWaterLevel | None = field(default=None, metadata={"dps": B01_Q10_DP.WATER_LEVEL}) | ||
| clean_count: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_COUNT}) | ||
| clean_mode: YXDeviceWorkMode | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_MODE}) | ||
| clean_task_type: YXDeviceCleanTask | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TASK_TYPE}) | ||
| back_type: YXBackType | None = field(default=None, metadata={"dps": B01_Q10_DP.BACK_TYPE}) | ||
| cleaning_progress: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEANING_PROGRESS}) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| """Common utilities for Q10 traits. | ||
| This module provides infrastructure for mapping Roborock Data Points (DPS) to | ||
| Python dataclass fields and handling the lifecycle of data updates from the | ||
| device. | ||
| ### DPS Metadata Annotation | ||
| Classes extending `RoborockBase` can annotate their fields with DPS IDs using | ||
| the `field(metadata={"dps": ...})` convention. This creates a declarative | ||
| mapping that `DpsDataConverter` uses to automatically route incoming device | ||
| data to the correct attribute. | ||
| Example: | ||
| ```python | ||
| @dataclass | ||
| class MyStatus(RoborockBase): | ||
| battery: int = field(metadata={"dps": B01_Q10_DP.BATTERY}) | ||
| ``` | ||
| ### Update Lifecycle | ||
| 1. **Raw Data**: The device sends encoded DPS updates over MQTT. | ||
| 2. **Decoding**: The transport layer decodes these into a dictionary (e.g., `{"101": 80}`). | ||
| 3. **Conversion**: `DpsDataConverter` uses `RoborockBase.convert_dict` to transform | ||
| raw values into appropriate Python types (e.g., Enums, ints) based on the | ||
| dataclass field types. | ||
| 4. **Update**: `update_from_dps` maps these converted values to field names and | ||
| updates the target object using `setattr`. | ||
| ### Usage | ||
| Typically, a trait will instantiate a single `DpsDataConverter` for its status class | ||
| and call `update_from_dps` whenever new data is received from the device stream. | ||
| """ | ||
|
|
||
| import dataclasses | ||
| from typing import Any | ||
|
|
||
| from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP | ||
| from roborock.data.containers import RoborockBase | ||
|
|
||
|
|
||
| class DpsDataConverter: | ||
| """Utility to handle the transformation and merging of DPS data into models. | ||
| This class pre-calculates the mapping between Data Point IDs and dataclass fields | ||
| to optimize repeated updates from device streams. | ||
| """ | ||
|
|
||
| def __init__(self, dps_type_map: dict[B01_Q10_DP, type], dps_field_map: dict[B01_Q10_DP, str]): | ||
| """Initialize the converter for a specific RoborockBase-derived class.""" | ||
| self._dps_type_map = dps_type_map | ||
| self._dps_field_map = dps_field_map | ||
|
|
||
| @classmethod | ||
| def from_dataclass(cls, dataclass_type: type[RoborockBase]): | ||
| """Initialize the converter for a specific RoborockBase-derived class.""" | ||
| dps_type_map: dict[B01_Q10_DP, type] = {} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing wrong here - just stylistically(the naming), B01_Q10_DP feels off. I think it was me who originally added it, but maybe we should change as a follow up
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think just like |
||
| dps_field_map: dict[B01_Q10_DP, str] = {} | ||
| for field_obj in dataclasses.fields(dataclass_type): | ||
| if field_obj.metadata and "dps" in field_obj.metadata: | ||
| dps_id = field_obj.metadata["dps"] | ||
| dps_type_map[dps_id] = field_obj.type | ||
| dps_field_map[dps_id] = field_obj.name | ||
| return cls(dps_type_map, dps_field_map) | ||
|
|
||
| def update_from_dps(self, target: RoborockBase, decoded_dps: dict[B01_Q10_DP, Any]) -> None: | ||
| """Convert and merge raw DPS data into the target object. | ||
| Uses the pre-calculated type mapping to ensure values are converted to the | ||
| correct Python types before being updated on the target. | ||
| Args: | ||
| target: The target object to update. | ||
| decoded_dps: The decoded DPS data to convert. | ||
| """ | ||
| conversions = RoborockBase.convert_dict(self._dps_type_map, decoded_dps) | ||
| for dps_id, value in conversions.items(): | ||
| field_name = self._dps_field_map[dps_id] | ||
| setattr(target, field_name, value) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed here - but I wonder if there is a way we can avoid having to do dataclass. Pydantic base models don't, but we do which is odd