|
| 1 | +"""Common utilities for Q10 traits. |
| 2 | +
|
| 3 | +This module provides infrastructure for mapping Roborock Data Points (DPS) to |
| 4 | +Python dataclass fields and handling the lifecycle of data updates from the |
| 5 | +device. |
| 6 | +
|
| 7 | +### DPS Metadata Annotation |
| 8 | +
|
| 9 | +Classes extending `RoborockBase` can annotate their fields with DPS IDs using |
| 10 | +the `field(metadata={"dps": ...})` convention. This creates a declarative |
| 11 | +mapping that `DpsDataConverter` uses to automatically route incoming device |
| 12 | +data to the correct attribute. |
| 13 | +
|
| 14 | +Example: |
| 15 | +
|
| 16 | +```python |
| 17 | +@dataclass |
| 18 | +class MyStatus(RoborockBase): |
| 19 | + battery: int = field(metadata={"dps": B01_Q10_DP.BATTERY}) |
| 20 | +``` |
| 21 | +
|
| 22 | +### Update Lifecycle |
| 23 | +1. **Raw Data**: The device sends encoded DPS updates over MQTT. |
| 24 | +2. **Decoding**: The transport layer decodes these into a dictionary (e.g., `{"101": 80}`). |
| 25 | +3. **Conversion**: `DpsDataConverter` uses `RoborockBase.convert_dict` to transform |
| 26 | + raw values into appropriate Python types (e.g., Enums, ints) based on the |
| 27 | + dataclass field types. |
| 28 | +4. **Update**: `update_from_dps` maps these converted values to field names and |
| 29 | + updates the target object using `setattr`. |
| 30 | +
|
| 31 | +### Usage |
| 32 | +
|
| 33 | +Typically, a trait will instantiate a single `DpsDataConverter` for its status class |
| 34 | +and call `update_from_dps` whenever new data is received from the device stream. |
| 35 | +
|
| 36 | +""" |
| 37 | + |
| 38 | +import dataclasses |
| 39 | +from typing import Any |
| 40 | + |
| 41 | +from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP |
| 42 | +from roborock.data.containers import RoborockBase |
| 43 | + |
| 44 | + |
| 45 | +class DpsDataConverter: |
| 46 | + """Utility to handle the transformation and merging of DPS data into models. |
| 47 | +
|
| 48 | + This class pre-calculates the mapping between Data Point IDs and dataclass fields |
| 49 | + to optimize repeated updates from device streams. |
| 50 | + """ |
| 51 | + |
| 52 | + def __init__(self, dps_type_map: dict[B01_Q10_DP, type], dps_field_map: dict[B01_Q10_DP, str]): |
| 53 | + """Initialize the converter for a specific RoborockBase-derived class.""" |
| 54 | + self._dps_type_map = dps_type_map |
| 55 | + self._dps_field_map = dps_field_map |
| 56 | + |
| 57 | + @classmethod |
| 58 | + def from_dataclass(cls, dataclass_type: type[RoborockBase]): |
| 59 | + """Initialize the converter for a specific RoborockBase-derived class.""" |
| 60 | + dps_type_map: dict[B01_Q10_DP, type] = {} |
| 61 | + dps_field_map: dict[B01_Q10_DP, str] = {} |
| 62 | + for field_obj in dataclasses.fields(dataclass_type): |
| 63 | + if field_obj.metadata and "dps" in field_obj.metadata: |
| 64 | + dps_id = field_obj.metadata["dps"] |
| 65 | + dps_type_map[dps_id] = field_obj.type |
| 66 | + dps_field_map[dps_id] = field_obj.name |
| 67 | + return cls(dps_type_map, dps_field_map) |
| 68 | + |
| 69 | + def update_from_dps(self, target: RoborockBase, decoded_dps: dict[B01_Q10_DP, Any]) -> None: |
| 70 | + """Convert and merge raw DPS data into the target object. |
| 71 | +
|
| 72 | + Uses the pre-calculated type mapping to ensure values are converted to the |
| 73 | + correct Python types before being updated on the target. |
| 74 | +
|
| 75 | + Args: |
| 76 | + target: The target object to update. |
| 77 | + decoded_dps: The decoded DPS data to convert. |
| 78 | + """ |
| 79 | + conversions = RoborockBase.convert_dict(self._dps_type_map, decoded_dps) |
| 80 | + for dps_id, value in conversions.items(): |
| 81 | + field_name = self._dps_field_map[dps_id] |
| 82 | + setattr(target, field_name, value) |
0 commit comments