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

API for interacting with B01 Q7 devices.

Q7PropertiesApi( channel: roborock.devices.transport.mqtt_channel.MqttChannel, map_rpc_channel: roborock.devices.rpc.b01_q7_channel.MapRpcChannel, device: roborock.data.containers.HomeDataDevice, product: roborock.data.containers.HomeDataProduct)
56    def __init__(
57        self, channel: MqttChannel, map_rpc_channel: MapRpcChannel, device: HomeDataDevice, product: HomeDataProduct
58    ) -> None:
59        """Initialize the Q7 API."""
60        self._channel = 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(channel)
69        self.map = MapTrait(channel)
70        self.map_content = MapContentTrait(
71            self._map_rpc_channel,
72            self.map,
73        )

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:
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)

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

async def set_prop( self, prop: roborock.roborock_message.RoborockB01Props, value: Any) -> None:
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        )

Set a property on the device.

async def set_fan_speed( self, fan_speed: roborock.data.b01_q7.b01_q7_code_mappings.SCWindMapping) -> None:
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)

Set the fan speed (wind).

async def set_water_level( self, water_level: roborock.data.b01_q7.b01_q7_code_mappings.WaterLevelMapping) -> None:
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)

Set the water level (water).

async def set_mode( self, mode: roborock.data.b01_q7.b01_q7_code_mappings.CleanTypeMapping) -> None:
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)

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:
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)

Set the cleaning path preference (route).

async def set_repeat_state( self, repeat: roborock.data.b01_q7.b01_q7_code_mappings.CleanRepeatMapping) -> None:
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)

Set the cleaning repeat state (cycles).

async def set_volume(self, volume: int) -> None:
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)

Set the robot voice volume (0-100).

async def set_child_lock(self, enabled: bool) -> None:
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))

Enable or disable the child lock.

async def set_do_not_disturb(self, enabled: bool, begin_time: int, end_time: int) -> None:
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        )

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:
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        )

Start cleaning.

async def clean_segments(self, segment_ids: list[int]) -> None:
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        )

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

async def pause_clean(self) -> None:
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        )

Pause cleaning.

async def stop_clean(self) -> None:
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        )

Stop cleaning.

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

Return to dock.

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

Locate the robot.

async def send( self, command: roborock.roborock_typing.RoborockB01Q7Methods | str, params: list | dict | int | None) -> Any:
201    async def send(self, command: CommandType, params: ParamsType) -> Any:
202        """Send a command to the device."""
203        return await send_decoded_command(
204            self._channel,
205            Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params),
206        )

Send a command to the device.

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

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

CleanSummaryTrait(channel: roborock.devices.transport.mqtt_channel.MqttChannel)
28    def __init__(self, channel: MqttChannel) -> None:
29        """Initialize the clean summary trait.
30
31        Args:
32            channel: MQTT channel used to communicate with the device.
33        """
34        super().__init__()
35        self._channel = channel

Initialize the clean summary trait.

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

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

Refresh totals and last record detail from the device.

13class MapTrait(Q7MapList, Trait):
14    """Map trait for B01/Q7 devices, responsible for fetching and caching map list metadata.
15
16    The MapContent is fetched from the MapContent trait, which relies on this trait to determine the
17    current map ID to fetch.
18    """
19
20    def __init__(self, channel: MqttChannel) -> None:
21        super().__init__()
22        self._channel = channel
23
24    async def refresh(self) -> None:
25        """Refresh cached map list metadata from the device."""
26        response = await send_decoded_command(
27            self._channel,
28            Q7RequestMessage(dps=B01_Q7_DPS, command=RoborockB01Q7Methods.GET_MAP_LIST, params={}),
29        )
30        if not isinstance(response, dict):
31            raise RoborockException(
32                f"Unexpected response type for GET_MAP_LIST: {type(response).__name__}: {response!r}"
33            )
34
35        if (parsed := Q7MapList.from_dict(response)) is None:
36            raise RoborockException(f"Failed to decode map list response: {response!r}")
37
38        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.transport.mqtt_channel.MqttChannel)
20    def __init__(self, channel: MqttChannel) -> None:
21        super().__init__()
22        self._channel = channel
async def refresh(self) -> None:
24    async def refresh(self) -> None:
25        """Refresh cached map list metadata from the device."""
26        response = await send_decoded_command(
27            self._channel,
28            Q7RequestMessage(dps=B01_Q7_DPS, command=RoborockB01Q7Methods.GET_MAP_LIST, params={}),
29        )
30        if not isinstance(response, dict):
31            raise RoborockException(
32                f"Unexpected response type for GET_MAP_LIST: {type(response).__name__}: {response!r}"
33            )
34
35        if (parsed := Q7MapList.from_dict(response)) is None:
36            raise RoborockException(f"Failed to decode map list response: {response!r}")
37
38        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):
 54class MapContentTrait(MapContent, Trait):
 55    """Trait for fetching parsed map content for Q7 devices."""
 56
 57    def __init__(
 58        self,
 59        map_rpc_channel: MapRpcChannel,
 60        map_trait: MapTrait,
 61        *,
 62        map_parser_config: B01MapParserConfig | None = None,
 63    ) -> None:
 64        super().__init__()
 65        self._map_rpc_channel = map_rpc_channel
 66        self._map_trait = map_trait
 67        self._map_parser = B01MapParser(map_parser_config)
 68        # Map uploads are serialized per-device to avoid response cross-wiring.
 69        self._map_command_lock = asyncio.Lock()
 70
 71    async def refresh(self) -> None:
 72        """Fetch, decode, and parse the current map payload.
 73
 74        This relies on the Map Trait already having fetched the map list metadata
 75        so it can determine the current map_id.
 76        """
 77        # Users must call first
 78        if (map_id := self._map_trait.current_map_id) is None:
 79            raise RoborockException("Unable to determine current map ID")
 80
 81        request = Q7RequestMessage(
 82            dps=B01_Q7_DPS,
 83            command=RoborockB01Q7Methods.UPLOAD_BY_MAPID,
 84            params={"map_id": map_id},
 85        )
 86        async with self._map_command_lock:
 87            raw_payload = await self._map_rpc_channel.send_map_command(request)
 88
 89        try:
 90            parsed_data = self._map_parser.parse(raw_payload)
 91        except RoborockException:
 92            raise
 93        except Exception as ex:
 94            raise RoborockException("Failed to parse B01 map data") from ex
 95
 96        if parsed_data.image_content is None:
 97            raise RoborockException("Failed to render B01 map image")
 98
 99        self.image_content = parsed_data.image_content
100        self.map_data = parsed_data.map_data
101        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.MapRpcChannel, map_trait: MapTrait, *, map_parser_config: roborock.map.b01_map_parser.B01MapParserConfig | None = None)
57    def __init__(
58        self,
59        map_rpc_channel: MapRpcChannel,
60        map_trait: MapTrait,
61        *,
62        map_parser_config: B01MapParserConfig | None = None,
63    ) -> None:
64        super().__init__()
65        self._map_rpc_channel = map_rpc_channel
66        self._map_trait = map_trait
67        self._map_parser = B01MapParser(map_parser_config)
68        # Map uploads are serialized per-device to avoid response cross-wiring.
69        self._map_command_lock = asyncio.Lock()
async def refresh(self) -> None:
 71    async def refresh(self) -> None:
 72        """Fetch, decode, and parse the current map payload.
 73
 74        This relies on the Map Trait already having fetched the map list metadata
 75        so it can determine the current map_id.
 76        """
 77        # Users must call first
 78        if (map_id := self._map_trait.current_map_id) is None:
 79            raise RoborockException("Unable to determine current map ID")
 80
 81        request = Q7RequestMessage(
 82            dps=B01_Q7_DPS,
 83            command=RoborockB01Q7Methods.UPLOAD_BY_MAPID,
 84            params={"map_id": map_id},
 85        )
 86        async with self._map_command_lock:
 87            raw_payload = await self._map_rpc_channel.send_map_command(request)
 88
 89        try:
 90            parsed_data = self._map_parser.parse(raw_payload)
 91        except RoborockException:
 92            raise
 93        except Exception as ex:
 94            raise RoborockException("Failed to parse B01 map data") from ex
 95
 96        if parsed_data.image_content is None:
 97            raise RoborockException("Failed to render B01 map image")
 98
 99        self.image_content = parsed_data.image_content
100        self.map_data = parsed_data.map_data
101        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