roborock.devices.traits.b01.q7

Traits for Q7 B01 devices.

Potentially other devices may fall into this category in the future.

  1"""Traits for Q7 B01 devices.
  2
  3Potentially other devices may fall into this category in the future.
  4"""
  5
  6from typing import Any
  7
  8from roborock import B01Props
  9from roborock.data import HomeDataDevice, HomeDataProduct, Q7MapList, Q7MapListEntry
 10from roborock.data.b01_q7.b01_q7_code_mappings import (
 11    CleanPathPreferenceMapping,
 12    CleanRepeatMapping,
 13    CleanTaskTypeMapping,
 14    CleanTypeMapping,
 15    SCDeviceCleanParam,
 16    SCWindMapping,
 17    WaterLevelMapping,
 18)
 19from roborock.devices.rpc.b01_q7_channel import Q7MapRpcChannel, Q7RpcChannel
 20from roborock.devices.traits import Trait
 21from roborock.exceptions import RoborockException
 22from roborock.protocols.b01_q7_protocol import CommandType, ParamsType
 23from roborock.roborock_message import RoborockB01Props
 24from roborock.roborock_typing import RoborockB01Q7Methods
 25
 26from .clean_summary import CleanSummaryTrait
 27from .map import MapTrait
 28from .map_content import MapContentTrait
 29
 30__all__ = [
 31    "Q7PropertiesApi",
 32    "CleanSummaryTrait",
 33    "MapTrait",
 34    "MapContentTrait",
 35    "Q7MapList",
 36    "Q7MapListEntry",
 37]
 38
 39
 40class Q7PropertiesApi(Trait):
 41    """API for interacting with B01 Q7 devices."""
 42
 43    clean_summary: CleanSummaryTrait
 44    """Trait for clean records / clean summary (Q7 `service.get_record_list`)."""
 45
 46    map: MapTrait
 47    """Trait for map list metadata + raw map payload retrieval."""
 48
 49    map_content: MapContentTrait
 50    """Trait for fetching parsed current map content."""
 51
 52    def __init__(
 53        self,
 54        rpc_channel: Q7RpcChannel,
 55        map_rpc_channel: Q7MapRpcChannel,
 56        device: HomeDataDevice,
 57        product: HomeDataProduct,
 58    ) -> None:
 59        """Initialize the Q7 API."""
 60        self._rpc_channel = rpc_channel
 61        self._map_rpc_channel = map_rpc_channel
 62        self._device = device
 63        self._product = product
 64
 65        if not device.sn or not product.model:
 66            raise ValueError("B01 Q7 map content requires device serial number and product model metadata")
 67
 68        self.clean_summary = CleanSummaryTrait(rpc_channel)
 69        self.map = MapTrait(rpc_channel)
 70        self.map_content = MapContentTrait(
 71            self._map_rpc_channel,
 72            self.map,
 73        )
 74
 75    async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
 76        """Query the device for the values of the given Q7 properties."""
 77        result = await self.send(
 78            RoborockB01Q7Methods.GET_PROP,
 79            {"property": props},
 80        )
 81        if not isinstance(result, dict):
 82            raise TypeError(f"Unexpected response type for GET_PROP: {type(result).__name__}: {result!r}")
 83        return B01Props.from_dict(result)
 84
 85    async def set_prop(self, prop: RoborockB01Props, value: Any) -> None:
 86        """Set a property on the device."""
 87        await self.send(
 88            command=RoborockB01Q7Methods.SET_PROP,
 89            params={prop: value},
 90        )
 91
 92    async def set_fan_speed(self, fan_speed: SCWindMapping) -> None:
 93        """Set the fan speed (wind)."""
 94        await self.set_prop(RoborockB01Props.WIND, fan_speed.code)
 95
 96    async def set_water_level(self, water_level: WaterLevelMapping) -> None:
 97        """Set the water level (water)."""
 98        await self.set_prop(RoborockB01Props.WATER, water_level.code)
 99
100    async def set_mode(self, mode: CleanTypeMapping) -> None:
101        """Set the cleaning mode (vacuum, mop, or vacuum and mop)."""
102        await self.set_prop(RoborockB01Props.MODE, mode.code)
103
104    async def set_clean_path_preference(self, preference: CleanPathPreferenceMapping) -> None:
105        """Set the cleaning path preference (route)."""
106        await self.set_prop(RoborockB01Props.CLEAN_PATH_PREFERENCE, preference.code)
107
108    async def set_repeat_state(self, repeat: CleanRepeatMapping) -> None:
109        """Set the cleaning repeat state (cycles)."""
110        await self.set_prop(RoborockB01Props.REPEAT_STATE, repeat.code)
111
112    async def set_volume(self, volume: int) -> None:
113        """Set the robot voice volume (0-100)."""
114        await self.set_prop(RoborockB01Props.VOLUME, volume)
115
116    async def set_child_lock(self, enabled: bool) -> None:
117        """Enable or disable the child lock."""
118        await self.set_prop(RoborockB01Props.CHILD_LOCK, int(enabled))
119
120    async def set_do_not_disturb(self, enabled: bool, begin_time: int, end_time: int) -> None:
121        """Configure do-not-disturb.
122
123        The device expects all three values together via ``service.set_quiet_time``
124        (individual ``prop.set`` calls are ignored). ``begin_time``/``end_time`` are
125        minutes since midnight and must be in the range 0-1439 (inclusive).
126
127        Ranges that cross midnight are supported by passing a ``begin_time`` that is
128        greater than ``end_time`` (e.g. 22:00-07:00 is ``begin_time=1320``,
129        ``end_time=420``).
130        """
131        for name, value in (("begin_time", begin_time), ("end_time", end_time)):
132            if not 0 <= value <= 1439:
133                raise ValueError(f"{name} must be between 0 and 1439 minutes since midnight, got {value}")
134        await self.send(
135            RoborockB01Q7Methods.SET_QUIET_TIME,
136            {
137                "is_open": int(enabled),
138                "quiet_begin_time": begin_time,
139                "quiet_end_time": end_time,
140            },
141        )
142
143    async def start_clean(self) -> None:
144        """Start cleaning."""
145        await self.send(
146            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
147            params={
148                "clean_type": CleanTaskTypeMapping.ALL.code,
149                "ctrl_value": SCDeviceCleanParam.START.code,
150                "room_ids": [],
151            },
152        )
153
154    async def clean_segments(self, segment_ids: list[int]) -> None:
155        """Start segment cleaning for the given ids (Q7 uses room ids)."""
156        await self.send(
157            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
158            params={
159                "clean_type": CleanTaskTypeMapping.ROOM.code,
160                "ctrl_value": SCDeviceCleanParam.START.code,
161                "room_ids": segment_ids,
162            },
163        )
164
165    async def pause_clean(self) -> None:
166        """Pause cleaning."""
167        await self.send(
168            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
169            params={
170                "clean_type": CleanTaskTypeMapping.ALL.code,
171                "ctrl_value": SCDeviceCleanParam.PAUSE.code,
172                "room_ids": [],
173            },
174        )
175
176    async def stop_clean(self) -> None:
177        """Stop cleaning."""
178        await self.send(
179            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
180            params={
181                "clean_type": CleanTaskTypeMapping.ALL.code,
182                "ctrl_value": SCDeviceCleanParam.STOP.code,
183                "room_ids": [],
184            },
185        )
186
187    async def return_to_dock(self) -> None:
188        """Return to dock."""
189        await self.send(
190            command=RoborockB01Q7Methods.START_RECHARGE,
191            params={},
192        )
193
194    async def find_me(self) -> None:
195        """Locate the robot."""
196        await self.send(
197            command=RoborockB01Q7Methods.FIND_DEVICE,
198            params={},
199        )
200
201    async def send(self, command: CommandType, params: ParamsType) -> Any:
202        """Send a command to the device."""
203        return await self._rpc_channel.send_command(command, params)
204
205
206def create(
207    product: HomeDataProduct,
208    device: HomeDataDevice,
209    rpc_channel: Q7RpcChannel,
210    map_rpc_channel: Q7MapRpcChannel,
211) -> Q7PropertiesApi:
212    """Create traits for B01 Q7 devices."""
213    if device.sn is None or product.model is None:
214        raise RoborockException(
215            f"Device serial number and product model are required (sn: {device.sn}, model: {product.model})"
216        )
217    return Q7PropertiesApi(
218        rpc_channel=rpc_channel,
219        map_rpc_channel=map_rpc_channel,
220        device=device,
221        product=product,
222    )
class Q7PropertiesApi(roborock.devices.traits.Trait):
 41class Q7PropertiesApi(Trait):
 42    """API for interacting with B01 Q7 devices."""
 43
 44    clean_summary: CleanSummaryTrait
 45    """Trait for clean records / clean summary (Q7 `service.get_record_list`)."""
 46
 47    map: MapTrait
 48    """Trait for map list metadata + raw map payload retrieval."""
 49
 50    map_content: MapContentTrait
 51    """Trait for fetching parsed current map content."""
 52
 53    def __init__(
 54        self,
 55        rpc_channel: Q7RpcChannel,
 56        map_rpc_channel: Q7MapRpcChannel,
 57        device: HomeDataDevice,
 58        product: HomeDataProduct,
 59    ) -> None:
 60        """Initialize the Q7 API."""
 61        self._rpc_channel = rpc_channel
 62        self._map_rpc_channel = map_rpc_channel
 63        self._device = device
 64        self._product = product
 65
 66        if not device.sn or not product.model:
 67            raise ValueError("B01 Q7 map content requires device serial number and product model metadata")
 68
 69        self.clean_summary = CleanSummaryTrait(rpc_channel)
 70        self.map = MapTrait(rpc_channel)
 71        self.map_content = MapContentTrait(
 72            self._map_rpc_channel,
 73            self.map,
 74        )
 75
 76    async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
 77        """Query the device for the values of the given Q7 properties."""
 78        result = await self.send(
 79            RoborockB01Q7Methods.GET_PROP,
 80            {"property": props},
 81        )
 82        if not isinstance(result, dict):
 83            raise TypeError(f"Unexpected response type for GET_PROP: {type(result).__name__}: {result!r}")
 84        return B01Props.from_dict(result)
 85
 86    async def set_prop(self, prop: RoborockB01Props, value: Any) -> None:
 87        """Set a property on the device."""
 88        await self.send(
 89            command=RoborockB01Q7Methods.SET_PROP,
 90            params={prop: value},
 91        )
 92
 93    async def set_fan_speed(self, fan_speed: SCWindMapping) -> None:
 94        """Set the fan speed (wind)."""
 95        await self.set_prop(RoborockB01Props.WIND, fan_speed.code)
 96
 97    async def set_water_level(self, water_level: WaterLevelMapping) -> None:
 98        """Set the water level (water)."""
 99        await self.set_prop(RoborockB01Props.WATER, water_level.code)
100
101    async def set_mode(self, mode: CleanTypeMapping) -> None:
102        """Set the cleaning mode (vacuum, mop, or vacuum and mop)."""
103        await self.set_prop(RoborockB01Props.MODE, mode.code)
104
105    async def set_clean_path_preference(self, preference: CleanPathPreferenceMapping) -> None:
106        """Set the cleaning path preference (route)."""
107        await self.set_prop(RoborockB01Props.CLEAN_PATH_PREFERENCE, preference.code)
108
109    async def set_repeat_state(self, repeat: CleanRepeatMapping) -> None:
110        """Set the cleaning repeat state (cycles)."""
111        await self.set_prop(RoborockB01Props.REPEAT_STATE, repeat.code)
112
113    async def set_volume(self, volume: int) -> None:
114        """Set the robot voice volume (0-100)."""
115        await self.set_prop(RoborockB01Props.VOLUME, volume)
116
117    async def set_child_lock(self, enabled: bool) -> None:
118        """Enable or disable the child lock."""
119        await self.set_prop(RoborockB01Props.CHILD_LOCK, int(enabled))
120
121    async def set_do_not_disturb(self, enabled: bool, begin_time: int, end_time: int) -> None:
122        """Configure do-not-disturb.
123
124        The device expects all three values together via ``service.set_quiet_time``
125        (individual ``prop.set`` calls are ignored). ``begin_time``/``end_time`` are
126        minutes since midnight and must be in the range 0-1439 (inclusive).
127
128        Ranges that cross midnight are supported by passing a ``begin_time`` that is
129        greater than ``end_time`` (e.g. 22:00-07:00 is ``begin_time=1320``,
130        ``end_time=420``).
131        """
132        for name, value in (("begin_time", begin_time), ("end_time", end_time)):
133            if not 0 <= value <= 1439:
134                raise ValueError(f"{name} must be between 0 and 1439 minutes since midnight, got {value}")
135        await self.send(
136            RoborockB01Q7Methods.SET_QUIET_TIME,
137            {
138                "is_open": int(enabled),
139                "quiet_begin_time": begin_time,
140                "quiet_end_time": end_time,
141            },
142        )
143
144    async def start_clean(self) -> None:
145        """Start cleaning."""
146        await self.send(
147            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
148            params={
149                "clean_type": CleanTaskTypeMapping.ALL.code,
150                "ctrl_value": SCDeviceCleanParam.START.code,
151                "room_ids": [],
152            },
153        )
154
155    async def clean_segments(self, segment_ids: list[int]) -> None:
156        """Start segment cleaning for the given ids (Q7 uses room ids)."""
157        await self.send(
158            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
159            params={
160                "clean_type": CleanTaskTypeMapping.ROOM.code,
161                "ctrl_value": SCDeviceCleanParam.START.code,
162                "room_ids": segment_ids,
163            },
164        )
165
166    async def pause_clean(self) -> None:
167        """Pause cleaning."""
168        await self.send(
169            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
170            params={
171                "clean_type": CleanTaskTypeMapping.ALL.code,
172                "ctrl_value": SCDeviceCleanParam.PAUSE.code,
173                "room_ids": [],
174            },
175        )
176
177    async def stop_clean(self) -> None:
178        """Stop cleaning."""
179        await self.send(
180            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
181            params={
182                "clean_type": CleanTaskTypeMapping.ALL.code,
183                "ctrl_value": SCDeviceCleanParam.STOP.code,
184                "room_ids": [],
185            },
186        )
187
188    async def return_to_dock(self) -> None:
189        """Return to dock."""
190        await self.send(
191            command=RoborockB01Q7Methods.START_RECHARGE,
192            params={},
193        )
194
195    async def find_me(self) -> None:
196        """Locate the robot."""
197        await self.send(
198            command=RoborockB01Q7Methods.FIND_DEVICE,
199            params={},
200        )
201
202    async def send(self, command: CommandType, params: ParamsType) -> Any:
203        """Send a command to the device."""
204        return await self._rpc_channel.send_command(command, params)

API for interacting with B01 Q7 devices.

Q7PropertiesApi( rpc_channel: roborock.devices.rpc.b01_q7_channel.Q7RpcChannel, map_rpc_channel: roborock.devices.rpc.b01_q7_channel.Q7MapRpcChannel, device: roborock.data.containers.HomeDataDevice, product: roborock.data.containers.HomeDataProduct)
53    def __init__(
54        self,
55        rpc_channel: Q7RpcChannel,
56        map_rpc_channel: Q7MapRpcChannel,
57        device: HomeDataDevice,
58        product: HomeDataProduct,
59    ) -> None:
60        """Initialize the Q7 API."""
61        self._rpc_channel = rpc_channel
62        self._map_rpc_channel = map_rpc_channel
63        self._device = device
64        self._product = product
65
66        if not device.sn or not product.model:
67            raise ValueError("B01 Q7 map content requires device serial number and product model metadata")
68
69        self.clean_summary = CleanSummaryTrait(rpc_channel)
70        self.map = MapTrait(rpc_channel)
71        self.map_content = MapContentTrait(
72            self._map_rpc_channel,
73            self.map,
74        )

Initialize the Q7 API.

clean_summary: CleanSummaryTrait

Trait for clean records / clean summary (Q7 service.get_record_list).

map: MapTrait

Trait for map list metadata + raw map payload retrieval.

map_content: MapContentTrait

Trait for fetching parsed current map content.

async def query_values( self, props: list[roborock.roborock_message.RoborockB01Props]) -> roborock.data.b01_q7.b01_q7_containers.B01Props | None:
76    async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
77        """Query the device for the values of the given Q7 properties."""
78        result = await self.send(
79            RoborockB01Q7Methods.GET_PROP,
80            {"property": props},
81        )
82        if not isinstance(result, dict):
83            raise TypeError(f"Unexpected response type for GET_PROP: {type(result).__name__}: {result!r}")
84        return B01Props.from_dict(result)

Query the device for the values of the given Q7 properties.

async def set_prop( self, prop: roborock.roborock_message.RoborockB01Props, value: Any) -> None:
86    async def set_prop(self, prop: RoborockB01Props, value: Any) -> None:
87        """Set a property on the device."""
88        await self.send(
89            command=RoborockB01Q7Methods.SET_PROP,
90            params={prop: value},
91        )

Set a property on the device.

async def set_fan_speed( self, fan_speed: roborock.data.b01_q7.b01_q7_code_mappings.SCWindMapping) -> None:
93    async def set_fan_speed(self, fan_speed: SCWindMapping) -> None:
94        """Set the fan speed (wind)."""
95        await self.set_prop(RoborockB01Props.WIND, fan_speed.code)

Set the fan speed (wind).

async def set_water_level( self, water_level: roborock.data.b01_q7.b01_q7_code_mappings.WaterLevelMapping) -> None:
97    async def set_water_level(self, water_level: WaterLevelMapping) -> None:
98        """Set the water level (water)."""
99        await self.set_prop(RoborockB01Props.WATER, water_level.code)

Set the water level (water).

async def set_mode( self, mode: roborock.data.b01_q7.b01_q7_code_mappings.CleanTypeMapping) -> None:
101    async def set_mode(self, mode: CleanTypeMapping) -> None:
102        """Set the cleaning mode (vacuum, mop, or vacuum and mop)."""
103        await self.set_prop(RoborockB01Props.MODE, mode.code)

Set the cleaning mode (vacuum, mop, or vacuum and mop).

async def set_clean_path_preference( self, preference: roborock.data.b01_q7.b01_q7_code_mappings.CleanPathPreferenceMapping) -> None:
105    async def set_clean_path_preference(self, preference: CleanPathPreferenceMapping) -> None:
106        """Set the cleaning path preference (route)."""
107        await self.set_prop(RoborockB01Props.CLEAN_PATH_PREFERENCE, preference.code)

Set the cleaning path preference (route).

async def set_repeat_state( self, repeat: roborock.data.b01_q7.b01_q7_code_mappings.CleanRepeatMapping) -> None:
109    async def set_repeat_state(self, repeat: CleanRepeatMapping) -> None:
110        """Set the cleaning repeat state (cycles)."""
111        await self.set_prop(RoborockB01Props.REPEAT_STATE, repeat.code)

Set the cleaning repeat state (cycles).

async def set_volume(self, volume: int) -> None:
113    async def set_volume(self, volume: int) -> None:
114        """Set the robot voice volume (0-100)."""
115        await self.set_prop(RoborockB01Props.VOLUME, volume)

Set the robot voice volume (0-100).

async def set_child_lock(self, enabled: bool) -> None:
117    async def set_child_lock(self, enabled: bool) -> None:
118        """Enable or disable the child lock."""
119        await self.set_prop(RoborockB01Props.CHILD_LOCK, int(enabled))

Enable or disable the child lock.

async def set_do_not_disturb(self, enabled: bool, begin_time: int, end_time: int) -> None:
121    async def set_do_not_disturb(self, enabled: bool, begin_time: int, end_time: int) -> None:
122        """Configure do-not-disturb.
123
124        The device expects all three values together via ``service.set_quiet_time``
125        (individual ``prop.set`` calls are ignored). ``begin_time``/``end_time`` are
126        minutes since midnight and must be in the range 0-1439 (inclusive).
127
128        Ranges that cross midnight are supported by passing a ``begin_time`` that is
129        greater than ``end_time`` (e.g. 22:00-07:00 is ``begin_time=1320``,
130        ``end_time=420``).
131        """
132        for name, value in (("begin_time", begin_time), ("end_time", end_time)):
133            if not 0 <= value <= 1439:
134                raise ValueError(f"{name} must be between 0 and 1439 minutes since midnight, got {value}")
135        await self.send(
136            RoborockB01Q7Methods.SET_QUIET_TIME,
137            {
138                "is_open": int(enabled),
139                "quiet_begin_time": begin_time,
140                "quiet_end_time": end_time,
141            },
142        )

Configure do-not-disturb.

The device expects all three values together via service.set_quiet_time (individual prop.set calls are ignored). begin_time/end_time are minutes since midnight and must be in the range 0-1439 (inclusive).

Ranges that cross midnight are supported by passing a begin_time that is greater than end_time (e.g. 22:00-07:00 is begin_time=1320, end_time=420).

async def start_clean(self) -> None:
144    async def start_clean(self) -> None:
145        """Start cleaning."""
146        await self.send(
147            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
148            params={
149                "clean_type": CleanTaskTypeMapping.ALL.code,
150                "ctrl_value": SCDeviceCleanParam.START.code,
151                "room_ids": [],
152            },
153        )

Start cleaning.

async def clean_segments(self, segment_ids: list[int]) -> None:
155    async def clean_segments(self, segment_ids: list[int]) -> None:
156        """Start segment cleaning for the given ids (Q7 uses room ids)."""
157        await self.send(
158            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
159            params={
160                "clean_type": CleanTaskTypeMapping.ROOM.code,
161                "ctrl_value": SCDeviceCleanParam.START.code,
162                "room_ids": segment_ids,
163            },
164        )

Start segment cleaning for the given ids (Q7 uses room ids).

async def pause_clean(self) -> None:
166    async def pause_clean(self) -> None:
167        """Pause cleaning."""
168        await self.send(
169            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
170            params={
171                "clean_type": CleanTaskTypeMapping.ALL.code,
172                "ctrl_value": SCDeviceCleanParam.PAUSE.code,
173                "room_ids": [],
174            },
175        )

Pause cleaning.

async def stop_clean(self) -> None:
177    async def stop_clean(self) -> None:
178        """Stop cleaning."""
179        await self.send(
180            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
181            params={
182                "clean_type": CleanTaskTypeMapping.ALL.code,
183                "ctrl_value": SCDeviceCleanParam.STOP.code,
184                "room_ids": [],
185            },
186        )

Stop cleaning.

async def return_to_dock(self) -> None:
188    async def return_to_dock(self) -> None:
189        """Return to dock."""
190        await self.send(
191            command=RoborockB01Q7Methods.START_RECHARGE,
192            params={},
193        )

Return to dock.

async def find_me(self) -> None:
195    async def find_me(self) -> None:
196        """Locate the robot."""
197        await self.send(
198            command=RoborockB01Q7Methods.FIND_DEVICE,
199            params={},
200        )

Locate the robot.

async def send( self, command: roborock.roborock_typing.RoborockB01Q7Methods | str, params: list | dict | int | None) -> Any:
202    async def send(self, command: CommandType, params: ParamsType) -> Any:
203        """Send a command to the device."""
204        return await self._rpc_channel.send_command(command, params)

Send a command to the device.

23class CleanSummaryTrait(CleanRecordSummary, Trait):
24    """B01/Q7 clean summary + clean record access (via record list service)."""
25
26    def __init__(self, channel: Q7RpcChannel) -> None:
27        """Initialize the clean summary trait.
28
29        Args:
30            channel: RPC channel used to communicate with the device.
31        """
32        super().__init__()
33        self._channel = channel
34
35    async def refresh(self) -> None:
36        """Refresh totals and last record detail from the device."""
37        record_list = await self._get_record_list()
38
39        self.total_time = record_list.total_time
40        self.total_area = record_list.total_area
41        self.total_count = record_list.total_count
42
43        details = await self._get_clean_record_details(record_list=record_list)
44        self.last_record_detail = details[0] if details else None
45
46    async def _get_record_list(self) -> CleanRecordList:
47        """Fetch the raw device clean record list (`service.get_record_list`)."""
48        result = await self._channel.send_command(
49            command=RoborockB01Q7Methods.GET_RECORD_LIST,
50            params={},
51        )
52
53        if not isinstance(result, dict):
54            raise TypeError(f"Unexpected response type for GET_RECORD_LIST: {type(result).__name__}: {result!r}")
55        return CleanRecordList.from_dict(result)
56
57    async def _get_clean_record_details(self, *, record_list: CleanRecordList) -> list[CleanRecordDetail]:
58        """Return parsed record detail objects (newest-first)."""
59        details: list[CleanRecordDetail] = []
60        for item in record_list.record_list:
61            try:
62                parsed = item.detail_parsed
63            except RoborockException as ex:
64                # Rather than failing if something goes wrong here, we should fail and log to tell the user.
65                _LOGGER.debug("Failed to parse record detail: %s", ex)
66                continue
67            if parsed is not None:
68                details.append(parsed)
69
70        # The server returns the newest record at the end of record_list; reverse so newest is first (index 0).
71        details.reverse()
72        return details

B01/Q7 clean summary + clean record access (via record list service).

CleanSummaryTrait(channel: roborock.devices.rpc.b01_q7_channel.Q7RpcChannel)
26    def __init__(self, channel: Q7RpcChannel) -> None:
27        """Initialize the clean summary trait.
28
29        Args:
30            channel: RPC channel used to communicate with the device.
31        """
32        super().__init__()
33        self._channel = channel

Initialize the clean summary trait.

Args: channel: RPC channel used to communicate with the device.

async def refresh(self) -> None:
35    async def refresh(self) -> None:
36        """Refresh totals and last record detail from the device."""
37        record_list = await self._get_record_list()
38
39        self.total_time = record_list.total_time
40        self.total_area = record_list.total_area
41        self.total_count = record_list.total_count
42
43        details = await self._get_clean_record_details(record_list=record_list)
44        self.last_record_detail = details[0] if details else None

Refresh totals and last record detail from the device.

11class MapTrait(Q7MapList, Trait):
12    """Map trait for B01/Q7 devices, responsible for fetching and caching map list metadata.
13
14    The MapContent is fetched from the MapContent trait, which relies on this trait to determine the
15    current map ID to fetch.
16    """
17
18    def __init__(self, channel: Q7RpcChannel) -> None:
19        super().__init__()
20        self._channel = channel
21
22    async def refresh(self) -> None:
23        """Refresh cached map list metadata from the device."""
24        response = await self._channel.send_command(
25            command=RoborockB01Q7Methods.GET_MAP_LIST,
26            params={},
27        )
28        if not isinstance(response, dict):
29            raise RoborockException(
30                f"Unexpected response type for GET_MAP_LIST: {type(response).__name__}: {response!r}"
31            )
32
33        if (parsed := Q7MapList.from_dict(response)) is None:
34            raise RoborockException(f"Failed to decode map list response: {response!r}")
35
36        self.map_list = parsed.map_list

Map trait for B01/Q7 devices, responsible for fetching and caching map list metadata.

The MapContent is fetched from the MapContent trait, which relies on this trait to determine the current map ID to fetch.

MapTrait(channel: roborock.devices.rpc.b01_q7_channel.Q7RpcChannel)
18    def __init__(self, channel: Q7RpcChannel) -> None:
19        super().__init__()
20        self._channel = channel
async def refresh(self) -> None:
22    async def refresh(self) -> None:
23        """Refresh cached map list metadata from the device."""
24        response = await self._channel.send_command(
25            command=RoborockB01Q7Methods.GET_MAP_LIST,
26            params={},
27        )
28        if not isinstance(response, dict):
29            raise RoborockException(
30                f"Unexpected response type for GET_MAP_LIST: {type(response).__name__}: {response!r}"
31            )
32
33        if (parsed := Q7MapList.from_dict(response)) is None:
34            raise RoborockException(f"Failed to decode map list response: {response!r}")
35
36        self.map_list = parsed.map_list

Refresh cached map list metadata from the device.

class MapContentTrait(roborock.devices.traits.b01.q7.map_content.MapContent, roborock.devices.traits.Trait):
53class MapContentTrait(MapContent, Trait):
54    """Trait for fetching parsed map content for Q7 devices."""
55
56    def __init__(
57        self,
58        map_rpc_channel: Q7MapRpcChannel,
59        map_trait: MapTrait,
60        *,
61        map_parser_config: B01MapParserConfig | None = None,
62    ) -> None:
63        super().__init__()
64        self._map_rpc_channel = map_rpc_channel
65        self._map_trait = map_trait
66        self._map_parser = B01MapParser(map_parser_config)
67        # Map uploads are serialized per-device to avoid response cross-wiring.
68        self._map_command_lock = asyncio.Lock()
69
70    async def refresh(self) -> None:
71        """Fetch, decode, and parse the current map payload.
72
73        This relies on the Map Trait already having fetched the map list metadata
74        so it can determine the current map_id.
75        """
76        # Users must call first
77        if (map_id := self._map_trait.current_map_id) is None:
78            raise RoborockException("Unable to determine current map ID")
79
80        async with self._map_command_lock:
81            raw_payload = await self._map_rpc_channel.send_map_command(
82                RoborockB01Q7Methods.UPLOAD_BY_MAPID,
83                {"map_id": map_id},
84            )
85
86        try:
87            parsed_data = self._map_parser.parse(raw_payload)
88        except RoborockException:
89            raise
90        except Exception as ex:
91            raise RoborockException("Failed to parse B01 map data") from ex
92
93        if parsed_data.image_content is None:
94            raise RoborockException("Failed to render B01 map image")
95
96        self.image_content = parsed_data.image_content
97        self.map_data = parsed_data.map_data
98        self.raw_api_response = raw_payload

Trait for fetching parsed map content for Q7 devices.

MapContentTrait( map_rpc_channel: roborock.devices.rpc.b01_q7_channel.Q7MapRpcChannel, map_trait: MapTrait, *, map_parser_config: roborock.map.b01_map_parser.B01MapParserConfig | None = None)
56    def __init__(
57        self,
58        map_rpc_channel: Q7MapRpcChannel,
59        map_trait: MapTrait,
60        *,
61        map_parser_config: B01MapParserConfig | None = None,
62    ) -> None:
63        super().__init__()
64        self._map_rpc_channel = map_rpc_channel
65        self._map_trait = map_trait
66        self._map_parser = B01MapParser(map_parser_config)
67        # Map uploads are serialized per-device to avoid response cross-wiring.
68        self._map_command_lock = asyncio.Lock()
async def refresh(self) -> None:
70    async def refresh(self) -> None:
71        """Fetch, decode, and parse the current map payload.
72
73        This relies on the Map Trait already having fetched the map list metadata
74        so it can determine the current map_id.
75        """
76        # Users must call first
77        if (map_id := self._map_trait.current_map_id) is None:
78            raise RoborockException("Unable to determine current map ID")
79
80        async with self._map_command_lock:
81            raw_payload = await self._map_rpc_channel.send_map_command(
82                RoborockB01Q7Methods.UPLOAD_BY_MAPID,
83                {"map_id": map_id},
84            )
85
86        try:
87            parsed_data = self._map_parser.parse(raw_payload)
88        except RoborockException:
89            raise
90        except Exception as ex:
91            raise RoborockException("Failed to parse B01 map data") from ex
92
93        if parsed_data.image_content is None:
94            raise RoborockException("Failed to render B01 map image")
95
96        self.image_content = parsed_data.image_content
97        self.map_data = parsed_data.map_data
98        self.raw_api_response = raw_payload

Fetch, decode, and parse the current map payload.

This relies on the Map Trait already having fetched the map list metadata so it can determine the current map_id.

@dataclass
class Q7MapList(roborock.data.containers.RoborockBase):
 87@dataclass
 88class Q7MapList(RoborockBase):
 89    """Map list response returned by `service.get_map_list`."""
 90
 91    map_list: list[Q7MapListEntry] = field(default_factory=list)
 92
 93    @property
 94    def current_map_id(self) -> int | None:
 95        """Current map id, preferring the entry marked current."""
 96        if not self.map_list:
 97            return None
 98
 99        ordered = sorted(self.map_list, key=lambda entry: entry.cur or False, reverse=True)
100        first = next(iter(ordered), None)
101        if first is None or not isinstance(first.id, int):
102            return None
103        return first.id

Map list response returned by service.get_map_list.

Q7MapList( map_list: list[Q7MapListEntry] = <factory>)
map_list: list[Q7MapListEntry]
current_map_id: int | None
 93    @property
 94    def current_map_id(self) -> int | None:
 95        """Current map id, preferring the entry marked current."""
 96        if not self.map_list:
 97            return None
 98
 99        ordered = sorted(self.map_list, key=lambda entry: entry.cur or False, reverse=True)
100        first = next(iter(ordered), None)
101        if first is None or not isinstance(first.id, int):
102            return None
103        return first.id

Current map id, preferring the entry marked current.

@dataclass
class Q7MapListEntry(roborock.data.containers.RoborockBase):
79@dataclass
80class Q7MapListEntry(RoborockBase):
81    """Single map list entry returned by `service.get_map_list`."""
82
83    id: int | None = None
84    cur: bool | None = None

Single map list entry returned by service.get_map_list.

Q7MapListEntry(id: int | None = None, cur: bool | None = None)
id: int | None = None
cur: bool | None = None