|
| 1 | +"""Clean summary / clean records trait for B01 Q7 devices. |
| 2 | +
|
| 3 | +For B01/Q7, the Roborock app uses `service.get_record_list` which returns totals |
| 4 | +and a `record_list` whose items contain a JSON string in `detail`. |
| 5 | +""" |
| 6 | + |
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +import json |
| 10 | + |
| 11 | +from roborock import CleanRecordDetail, CleanRecordList, CleanRecordSummary |
| 12 | +from roborock.devices.rpc.b01_q7_channel import send_decoded_command |
| 13 | +from roborock.devices.traits import Trait |
| 14 | +from roborock.devices.transport.mqtt_channel import MqttChannel |
| 15 | +from roborock.exceptions import RoborockException |
| 16 | +from roborock.protocols.b01_q7_protocol import Q7RequestMessage |
| 17 | +from roborock.roborock_typing import RoborockB01Q7Methods |
| 18 | + |
| 19 | +__all__ = [ |
| 20 | + "CleanSummaryTrait", |
| 21 | +] |
| 22 | + |
| 23 | + |
| 24 | +class CleanSummaryTrait(CleanRecordSummary, Trait): |
| 25 | + """B01/Q7 clean summary + clean record access (via record list service).""" |
| 26 | + |
| 27 | + def __init__(self, channel: MqttChannel) -> None: |
| 28 | + super().__init__() |
| 29 | + self._channel = channel |
| 30 | + |
| 31 | + async def refresh(self) -> None: |
| 32 | + """Refresh totals and last record detail from the device.""" |
| 33 | + record_list = await self.get_record_list() |
| 34 | + |
| 35 | + self.total_time = record_list.total_time |
| 36 | + self.total_area = record_list.total_area |
| 37 | + self.total_count = record_list.total_count |
| 38 | + |
| 39 | + details = await self.get_clean_record_details(record_list=record_list) |
| 40 | + self.last_record_detail = details[0] if details else None |
| 41 | + |
| 42 | + async def get_record_list(self) -> CleanRecordList: |
| 43 | + """Fetch the raw device clean record list (`service.get_record_list`).""" |
| 44 | + result = await send_decoded_command( |
| 45 | + self._channel, |
| 46 | + Q7RequestMessage(dps=10000, command=RoborockB01Q7Methods.GET_RECORD_LIST, params={}), |
| 47 | + ) |
| 48 | + |
| 49 | + if not isinstance(result, dict): |
| 50 | + raise TypeError(f"Unexpected response type for GET_RECORD_LIST: {type(result).__name__}: {result!r}") |
| 51 | + return CleanRecordList.from_dict(result) |
| 52 | + |
| 53 | + @staticmethod |
| 54 | + def _parse_record_detail(detail: dict | str | None) -> CleanRecordDetail | None: |
| 55 | + if detail is None: |
| 56 | + return None |
| 57 | + if isinstance(detail, str): |
| 58 | + try: |
| 59 | + parsed = json.loads(detail) |
| 60 | + except json.JSONDecodeError as ex: |
| 61 | + raise RoborockException(f"Invalid B01 record detail JSON: {detail!r}") from ex |
| 62 | + if not isinstance(parsed, dict): |
| 63 | + raise RoborockException(f"Unexpected B01 record detail type: {type(parsed).__name__}: {parsed!r}") |
| 64 | + return CleanRecordDetail.from_dict(parsed) |
| 65 | + if isinstance(detail, dict): |
| 66 | + return CleanRecordDetail.from_dict(detail) |
| 67 | + raise TypeError(f"Unexpected B01 record detail type: {type(detail).__name__}: {detail!r}") |
| 68 | + |
| 69 | + async def get_clean_record_details(self, *, record_list: CleanRecordList | None = None) -> list[CleanRecordDetail]: |
| 70 | + """Return parsed record detail objects (newest-first).""" |
| 71 | + if record_list is None: |
| 72 | + record_list = await self.get_record_list() |
| 73 | + |
| 74 | + details: list[CleanRecordDetail] = [] |
| 75 | + for item in record_list.record_list: |
| 76 | + parsed = self._parse_record_detail(item.detail) |
| 77 | + if parsed is not None: |
| 78 | + details.append(parsed) |
| 79 | + |
| 80 | + # App treats the newest record as the end of the list |
| 81 | + details.reverse() |
| 82 | + return details |
0 commit comments