roborock.devices.traits.b01.q10

Traits for Q10 B01 devices.

  1"""Traits for Q10 B01 devices."""
  2
  3import asyncio
  4import logging
  5from typing import Any
  6
  7from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
  8from roborock.data.containers import RoborockBase
  9from roborock.devices.rpc.b01_q10_channel import B01Q10Channel
 10from roborock.devices.traits import Trait
 11from roborock.map.b01_q10_map_parser import (
 12    B01Q10MapParserConfig,
 13    Q10CleanRecordDetail,
 14    Q10MapPacket,
 15    Q10MapPacketKind,
 16    Q10TracePacket,
 17)
 18from roborock.protocols.b01_q10_protocol import Q10DpsUpdate, Q10Message
 19
 20from .button_light import ButtonLightTrait
 21from .child_lock import ChildLockTrait
 22from .clean_history import CleanHistoryTrait
 23from .command import CommandTrait
 24from .consumable import ConsumableTrait
 25from .do_not_disturb import DoNotDisturbTrait
 26from .dust_collection import DustCollectionTrait
 27from .map import MapContentTrait, MapDpsTrait
 28from .maps import MapsTrait
 29from .network_info import NetworkInfoTrait
 30from .remote import RemoteTrait
 31from .status import StatusTrait
 32from .vacuum import VacuumTrait
 33from .volume import SoundVolumeTrait
 34
 35__all__ = [
 36    "Q10PropertiesApi",
 37    "ButtonLightTrait",
 38    "ChildLockTrait",
 39    "CleanHistoryTrait",
 40    "ConsumableTrait",
 41    "DoNotDisturbTrait",
 42    "DustCollectionTrait",
 43    "MapContentTrait",
 44    "MapsTrait",
 45    "NetworkInfoTrait",
 46    "SoundVolumeTrait",
 47    "StatusTrait",
 48]
 49
 50_LOGGER = logging.getLogger(__name__)
 51
 52
 53class Q10PropertiesApi(Trait):
 54    """API for interacting with B01 devices."""
 55
 56    command: CommandTrait
 57    """Trait for sending commands to Q10 devices."""
 58
 59    status: StatusTrait
 60    """Trait for managing the core status of Q10 devices."""
 61
 62    vacuum: VacuumTrait
 63    """Trait for sending vacuum related commands to Q10 devices."""
 64
 65    remote: RemoteTrait
 66    """Trait for sending remote control related commands to Q10 devices."""
 67
 68    volume: SoundVolumeTrait
 69    """Trait for reading / setting the speaker volume."""
 70
 71    child_lock: ChildLockTrait
 72    """Trait for reading / controlling the child lock."""
 73
 74    do_not_disturb: DoNotDisturbTrait
 75    """Trait for reading / controlling Do Not Disturb."""
 76
 77    dust_collection: DustCollectionTrait
 78    """Trait for reading / controlling dock auto-empty (dust collection)."""
 79
 80    button_light: ButtonLightTrait
 81    """Trait for controlling the indicator / button light (LED)."""
 82
 83    network_info: NetworkInfoTrait
 84    """Trait exposing the device's network information."""
 85
 86    consumable: ConsumableTrait
 87    """Trait exposing remaining life of consumables."""
 88
 89    map: MapContentTrait
 90    """Composed map image plus caller-facing map and trace data."""
 91
 92    maps: MapsTrait
 93    """Saved-map list metadata."""
 94
 95    _map_dps: MapDpsTrait
 96    """Private source of restricted zones and virtual walls received through DPS."""
 97
 98    clean_history: CleanHistoryTrait
 99    """Trait for fetching the device clean-record history (``dpCleanRecord``)."""
100
101    def __init__(
102        self,
103        channel: B01Q10Channel,
104        *,
105        map_parser_config: B01Q10MapParserConfig | None = None,
106    ) -> None:
107        """Initialize the B01Props API."""
108        self._channel = channel
109        self.command = CommandTrait(channel)
110        self.remote = RemoteTrait(self.command)
111        self.status = StatusTrait()
112        self.volume = SoundVolumeTrait(self.command)
113        self.child_lock = ChildLockTrait(self.command)
114        self.do_not_disturb = DoNotDisturbTrait(self.command)
115        self.dust_collection = DustCollectionTrait(self.command)
116        self.button_light = ButtonLightTrait(self.command)
117        self.network_info = NetworkInfoTrait()
118        self.consumable = ConsumableTrait()
119        self._map_dps = MapDpsTrait()
120        self.maps = MapsTrait(self.command)
121        self.map = MapContentTrait(
122            self._map_dps,
123            self.command,
124            map_parser_config=map_parser_config,
125        )
126        self.clean_history = CleanHistoryTrait(self.command)
127        self.vacuum = VacuumTrait(self.command, self.status, self.map)
128        # Read-model traits updated from the device's DPS push stream.
129        self._updatable_traits = [
130            self.status,
131            self.volume,
132            self.child_lock,
133            self.do_not_disturb,
134            self.dust_collection,
135            self.network_info,
136            self.consumable,
137            self.clean_history,
138            self._map_dps,
139            self.maps,
140        ]
141        self._subscribe_task: asyncio.Task[None] | None = None
142
143    async def start(self) -> None:
144        """Start any necessary subscriptions for the trait."""
145        self._subscribe_task = asyncio.create_task(self._subscribe_loop())
146
147    async def close(self) -> None:
148        """Close any resources held by the trait."""
149        await self.vacuum.close()
150        if self._subscribe_task is not None:
151            self._subscribe_task.cancel()
152            try:
153                await self._subscribe_task
154            except asyncio.CancelledError:
155                pass  # ignore cancellation errors
156            self._subscribe_task = None
157
158    async def refresh(self) -> None:
159        """Refresh all traits."""
160        # Sending REQUEST_DPS causes the device to publish its ordinary status
161        # values. Map-list and map-content refreshes have separate schedules.
162        await self.command.send(B01_Q10_DP.REQUEST_DPS, params={})
163
164    async def _subscribe_loop(self) -> None:
165        """Persistent loop dispatching decoded messages to the read-model traits."""
166        async for message in self._channel.subscribe_stream():
167            self._handle_message(message)
168
169    def _handle_message(self, message: Q10Message) -> None:
170        """Route a single decoded message to the trait responsible for it.
171
172        Map and trace packets arrive as protocol-301 ``MAP_RESPONSE`` pushes.
173        Map-list DPS responses and other DPS updates feed the read-model traits.
174        """
175        if isinstance(message, Q10MapPacket):
176            if message.kind is Q10MapPacketKind.CURRENT:
177                self.map.update_from_map_packet(message)
178            elif message.kind is Q10MapPacketKind.SAVED_MAP_DETAIL:
179                self.maps.update_from_map_packet(message)
180        elif isinstance(message, Q10CleanRecordDetail):
181            self.clean_history.update_from_detail(message)
182        elif isinstance(message, Q10TracePacket):
183            self.map.update_from_trace_packet(message)
184        elif isinstance(message, Q10DpsUpdate):
185            _LOGGER.debug("Received Q10 status update: %s", message.dps)
186            # Notify all read-model traits about the new message; each trait
187            # only updates the fields that it is responsible for.
188            for trait in self._updatable_traits:
189                trait.update_from_dps(message.dps)
190
191    def as_dict(self) -> dict[str, Any]:
192        """Return the trait data as a dictionary."""
193        result: dict[str, Any] = {}
194        for name, value in self.__dict__.items():
195            if isinstance(value, RoborockBase) and not name.startswith("_"):
196                result[name] = value.as_dict()
197        if hasattr(self, "map") and hasattr(self.map, "as_dict"):
198            result["map"] = self.map.as_dict()
199        return result
200
201
202def create(
203    channel: B01Q10Channel,
204    *,
205    map_parser_config: B01Q10MapParserConfig | None = None,
206) -> Q10PropertiesApi:
207    """Create traits for B01 devices."""
208    return Q10PropertiesApi(
209        channel,
210        map_parser_config=map_parser_config,
211    )
class Q10PropertiesApi(roborock.devices.traits.Trait):
 54class Q10PropertiesApi(Trait):
 55    """API for interacting with B01 devices."""
 56
 57    command: CommandTrait
 58    """Trait for sending commands to Q10 devices."""
 59
 60    status: StatusTrait
 61    """Trait for managing the core status of Q10 devices."""
 62
 63    vacuum: VacuumTrait
 64    """Trait for sending vacuum related commands to Q10 devices."""
 65
 66    remote: RemoteTrait
 67    """Trait for sending remote control related commands to Q10 devices."""
 68
 69    volume: SoundVolumeTrait
 70    """Trait for reading / setting the speaker volume."""
 71
 72    child_lock: ChildLockTrait
 73    """Trait for reading / controlling the child lock."""
 74
 75    do_not_disturb: DoNotDisturbTrait
 76    """Trait for reading / controlling Do Not Disturb."""
 77
 78    dust_collection: DustCollectionTrait
 79    """Trait for reading / controlling dock auto-empty (dust collection)."""
 80
 81    button_light: ButtonLightTrait
 82    """Trait for controlling the indicator / button light (LED)."""
 83
 84    network_info: NetworkInfoTrait
 85    """Trait exposing the device's network information."""
 86
 87    consumable: ConsumableTrait
 88    """Trait exposing remaining life of consumables."""
 89
 90    map: MapContentTrait
 91    """Composed map image plus caller-facing map and trace data."""
 92
 93    maps: MapsTrait
 94    """Saved-map list metadata."""
 95
 96    _map_dps: MapDpsTrait
 97    """Private source of restricted zones and virtual walls received through DPS."""
 98
 99    clean_history: CleanHistoryTrait
100    """Trait for fetching the device clean-record history (``dpCleanRecord``)."""
101
102    def __init__(
103        self,
104        channel: B01Q10Channel,
105        *,
106        map_parser_config: B01Q10MapParserConfig | None = None,
107    ) -> None:
108        """Initialize the B01Props API."""
109        self._channel = channel
110        self.command = CommandTrait(channel)
111        self.remote = RemoteTrait(self.command)
112        self.status = StatusTrait()
113        self.volume = SoundVolumeTrait(self.command)
114        self.child_lock = ChildLockTrait(self.command)
115        self.do_not_disturb = DoNotDisturbTrait(self.command)
116        self.dust_collection = DustCollectionTrait(self.command)
117        self.button_light = ButtonLightTrait(self.command)
118        self.network_info = NetworkInfoTrait()
119        self.consumable = ConsumableTrait()
120        self._map_dps = MapDpsTrait()
121        self.maps = MapsTrait(self.command)
122        self.map = MapContentTrait(
123            self._map_dps,
124            self.command,
125            map_parser_config=map_parser_config,
126        )
127        self.clean_history = CleanHistoryTrait(self.command)
128        self.vacuum = VacuumTrait(self.command, self.status, self.map)
129        # Read-model traits updated from the device's DPS push stream.
130        self._updatable_traits = [
131            self.status,
132            self.volume,
133            self.child_lock,
134            self.do_not_disturb,
135            self.dust_collection,
136            self.network_info,
137            self.consumable,
138            self.clean_history,
139            self._map_dps,
140            self.maps,
141        ]
142        self._subscribe_task: asyncio.Task[None] | None = None
143
144    async def start(self) -> None:
145        """Start any necessary subscriptions for the trait."""
146        self._subscribe_task = asyncio.create_task(self._subscribe_loop())
147
148    async def close(self) -> None:
149        """Close any resources held by the trait."""
150        await self.vacuum.close()
151        if self._subscribe_task is not None:
152            self._subscribe_task.cancel()
153            try:
154                await self._subscribe_task
155            except asyncio.CancelledError:
156                pass  # ignore cancellation errors
157            self._subscribe_task = None
158
159    async def refresh(self) -> None:
160        """Refresh all traits."""
161        # Sending REQUEST_DPS causes the device to publish its ordinary status
162        # values. Map-list and map-content refreshes have separate schedules.
163        await self.command.send(B01_Q10_DP.REQUEST_DPS, params={})
164
165    async def _subscribe_loop(self) -> None:
166        """Persistent loop dispatching decoded messages to the read-model traits."""
167        async for message in self._channel.subscribe_stream():
168            self._handle_message(message)
169
170    def _handle_message(self, message: Q10Message) -> None:
171        """Route a single decoded message to the trait responsible for it.
172
173        Map and trace packets arrive as protocol-301 ``MAP_RESPONSE`` pushes.
174        Map-list DPS responses and other DPS updates feed the read-model traits.
175        """
176        if isinstance(message, Q10MapPacket):
177            if message.kind is Q10MapPacketKind.CURRENT:
178                self.map.update_from_map_packet(message)
179            elif message.kind is Q10MapPacketKind.SAVED_MAP_DETAIL:
180                self.maps.update_from_map_packet(message)
181        elif isinstance(message, Q10CleanRecordDetail):
182            self.clean_history.update_from_detail(message)
183        elif isinstance(message, Q10TracePacket):
184            self.map.update_from_trace_packet(message)
185        elif isinstance(message, Q10DpsUpdate):
186            _LOGGER.debug("Received Q10 status update: %s", message.dps)
187            # Notify all read-model traits about the new message; each trait
188            # only updates the fields that it is responsible for.
189            for trait in self._updatable_traits:
190                trait.update_from_dps(message.dps)
191
192    def as_dict(self) -> dict[str, Any]:
193        """Return the trait data as a dictionary."""
194        result: dict[str, Any] = {}
195        for name, value in self.__dict__.items():
196            if isinstance(value, RoborockBase) and not name.startswith("_"):
197                result[name] = value.as_dict()
198        if hasattr(self, "map") and hasattr(self.map, "as_dict"):
199            result["map"] = self.map.as_dict()
200        return result

API for interacting with B01 devices.

Q10PropertiesApi( channel: roborock.devices.rpc.b01_q10_channel.B01Q10Channel, *, map_parser_config: roborock.map.b01_q10_map_parser.B01Q10MapParserConfig | None = None)
102    def __init__(
103        self,
104        channel: B01Q10Channel,
105        *,
106        map_parser_config: B01Q10MapParserConfig | None = None,
107    ) -> None:
108        """Initialize the B01Props API."""
109        self._channel = channel
110        self.command = CommandTrait(channel)
111        self.remote = RemoteTrait(self.command)
112        self.status = StatusTrait()
113        self.volume = SoundVolumeTrait(self.command)
114        self.child_lock = ChildLockTrait(self.command)
115        self.do_not_disturb = DoNotDisturbTrait(self.command)
116        self.dust_collection = DustCollectionTrait(self.command)
117        self.button_light = ButtonLightTrait(self.command)
118        self.network_info = NetworkInfoTrait()
119        self.consumable = ConsumableTrait()
120        self._map_dps = MapDpsTrait()
121        self.maps = MapsTrait(self.command)
122        self.map = MapContentTrait(
123            self._map_dps,
124            self.command,
125            map_parser_config=map_parser_config,
126        )
127        self.clean_history = CleanHistoryTrait(self.command)
128        self.vacuum = VacuumTrait(self.command, self.status, self.map)
129        # Read-model traits updated from the device's DPS push stream.
130        self._updatable_traits = [
131            self.status,
132            self.volume,
133            self.child_lock,
134            self.do_not_disturb,
135            self.dust_collection,
136            self.network_info,
137            self.consumable,
138            self.clean_history,
139            self._map_dps,
140            self.maps,
141        ]
142        self._subscribe_task: asyncio.Task[None] | None = None

Initialize the B01Props API.

command: roborock.devices.traits.b01.q10.command.CommandTrait

Trait for sending commands to Q10 devices.

status: StatusTrait

Trait for managing the core status of Q10 devices.

vacuum: roborock.devices.traits.b01.q10.vacuum.VacuumTrait

Trait for sending vacuum related commands to Q10 devices.

remote: roborock.devices.traits.b01.q10.remote.RemoteTrait

Trait for sending remote control related commands to Q10 devices.

Trait for reading / setting the speaker volume.

child_lock: ChildLockTrait

Trait for reading / controlling the child lock.

do_not_disturb: DoNotDisturbTrait

Trait for reading / controlling Do Not Disturb.

dust_collection: DustCollectionTrait

Trait for reading / controlling dock auto-empty (dust collection).

button_light: ButtonLightTrait

Trait for controlling the indicator / button light (LED).

network_info: NetworkInfoTrait

Trait exposing the device's network information.

consumable: ConsumableTrait

Trait exposing remaining life of consumables.

Composed map image plus caller-facing map and trace data.

maps: MapsTrait

Saved-map list metadata.

clean_history: CleanHistoryTrait

Trait for fetching the device clean-record history (dpCleanRecord).

async def start(self) -> None:
144    async def start(self) -> None:
145        """Start any necessary subscriptions for the trait."""
146        self._subscribe_task = asyncio.create_task(self._subscribe_loop())

Start any necessary subscriptions for the trait.

async def close(self) -> None:
148    async def close(self) -> None:
149        """Close any resources held by the trait."""
150        await self.vacuum.close()
151        if self._subscribe_task is not None:
152            self._subscribe_task.cancel()
153            try:
154                await self._subscribe_task
155            except asyncio.CancelledError:
156                pass  # ignore cancellation errors
157            self._subscribe_task = None

Close any resources held by the trait.

async def refresh(self) -> None:
159    async def refresh(self) -> None:
160        """Refresh all traits."""
161        # Sending REQUEST_DPS causes the device to publish its ordinary status
162        # values. Map-list and map-content refreshes have separate schedules.
163        await self.command.send(B01_Q10_DP.REQUEST_DPS, params={})

Refresh all traits.

def as_dict(self) -> dict[str, typing.Any]:
192    def as_dict(self) -> dict[str, Any]:
193        """Return the trait data as a dictionary."""
194        result: dict[str, Any] = {}
195        for name, value in self.__dict__.items():
196            if isinstance(value, RoborockBase) and not name.startswith("_"):
197                result[name] = value.as_dict()
198        if hasattr(self, "map") and hasattr(self.map, "as_dict"):
199            result["map"] = self.map.as_dict()
200        return result

Return the trait data as a dictionary.

class ButtonLightTrait:
 9class ButtonLightTrait:
10    """Trait for controlling the indicator / button light (LED) of a Q10 device.
11
12    The device does not report the button-light state in its status dump, so
13    this trait is write-only (no read-back).
14    """
15
16    def __init__(self, command: CommandTrait) -> None:
17        """Initialize the button light trait."""
18        self._command = command
19
20    async def _write(self, value: int) -> None:
21        """Write the button-light data point via the dpCommon (101) wrapper."""
22        await self._command.send(B01_Q10_DP.COMMON, {str(B01_Q10_DP.BUTTON_LIGHT_SWITCH.code): value})
23
24    async def enable(self) -> None:
25        """Turn the indicator light on."""
26        await self._write(1)
27
28    async def disable(self) -> None:
29        """Turn the indicator light off."""
30        await self._write(0)

Trait for controlling the indicator / button light (LED) of a Q10 device.

The device does not report the button-light state in its status dump, so this trait is write-only (no read-back).

ButtonLightTrait(command: roborock.devices.traits.b01.q10.command.CommandTrait)
16    def __init__(self, command: CommandTrait) -> None:
17        """Initialize the button light trait."""
18        self._command = command

Initialize the button light trait.

async def enable(self) -> None:
24    async def enable(self) -> None:
25        """Turn the indicator light on."""
26        await self._write(1)

Turn the indicator light on.

async def disable(self) -> None:
28    async def disable(self) -> None:
29        """Turn the indicator light off."""
30        await self._write(0)

Turn the indicator light off.

class ChildLockTrait(roborock.data.b01_q10.b01_q10_containers.ChildLock, roborock.devices.traits.b01.q10.common.UpdatableTrait):
16class ChildLockTrait(ChildLock, UpdatableTrait):
17    """Trait for reading and controlling the child lock of a Q10 device."""
18
19    _CONVERTER = DpsDataConverter.from_dataclass(ChildLock)
20
21    def __init__(self, command: CommandTrait) -> None:
22        """Initialize the child lock trait."""
23        ChildLock.__init__(self)
24        UpdatableTrait.__init__(self, command, _LOGGER)
25
26    @property
27    def is_on(self) -> bool:
28        """Return whether the child lock is enabled."""
29        return bool(self.child_lock)
30
31    async def enable(self) -> None:
32        """Enable the child lock."""
33        await self._write(B01_Q10_DP.CHILD_LOCK, 1)
34
35    async def disable(self) -> None:
36        """Disable the child lock."""
37        await self._write(B01_Q10_DP.CHILD_LOCK, 0)

Trait for reading and controlling the child lock of a Q10 device.

ChildLockTrait(command: roborock.devices.traits.b01.q10.command.CommandTrait)
21    def __init__(self, command: CommandTrait) -> None:
22        """Initialize the child lock trait."""
23        ChildLock.__init__(self)
24        UpdatableTrait.__init__(self, command, _LOGGER)

Initialize the child lock trait.

is_on: bool
26    @property
27    def is_on(self) -> bool:
28        """Return whether the child lock is enabled."""
29        return bool(self.child_lock)

Return whether the child lock is enabled.

async def enable(self) -> None:
31    async def enable(self) -> None:
32        """Enable the child lock."""
33        await self._write(B01_Q10_DP.CHILD_LOCK, 1)

Enable the child lock.

async def disable(self) -> None:
35    async def disable(self) -> None:
36        """Disable the child lock."""
37        await self._write(B01_Q10_DP.CHILD_LOCK, 0)

Disable the child lock.

class CleanHistoryTrait(roborock.devices.traits.b01.q10.clean_history.CleanHistory, roborock.devices.traits.b01.q10.common.UpdatableTrait):
152class CleanHistoryTrait(CleanHistory, UpdatableTrait):
153    """Browse past cleaning runs and retrieve their parsed maps and historical paths.
154
155    Subscribe with ``add_update_listener`` to receive requested or unsolicited
156    archives. Archive pushes contain no record ID, so the latest detail cannot
157    be attributed to a particular selection.
158    """
159
160    _command: CommandTrait
161
162    def __init__(self, command: CommandTrait) -> None:
163        CleanHistory.__init__(self)
164        UpdatableTrait.__init__(self, command, _LOGGER)
165        self._command = command
166        self._converter = CleanRecordConverter()
167
168    async def refresh(self) -> None:
169        """Request the clean-record list from the device.
170
171        This sends the query and returns immediately; the records arrive
172        asynchronously on the device stream and populate :attr:`records` once
173        :meth:`update_from_dps` processes the ``dpCleanRecord`` push.
174        """
175        await self._command.send(
176            B01_Q10_DP.COMMON,
177            params={str(B01_Q10_DP.CLEAN_RECORD.code): {"op": "list"}},
178        )
179
180    async def refresh_detail(self, record: Q10CleanRecord) -> None:
181        """Request the saved map and path for a past cleaning run.
182
183        Returns after sending; listeners receive the archive separately. The
184        push contains no record ID to correlate it with this selection.
185        """
186        if not record.raw or not record.map_len:
187            raise RoborockException("The Q10 clean record has no saved map detail")
188        await self._command.send(
189            B01_Q10_DP.COMMON,
190            {str(B01_Q10_DP.CLEAN_RECORD.code): {"op": "select", "id": record.raw}},
191        )
192
193    def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None:
194        """Apply a ``dpCleanRecord`` push (a full list reply or a single notify)."""
195        envelope = decoded_dps.get(B01_Q10_DP.CLEAN_RECORD)
196        if not isinstance(envelope, dict):
197            return
198        push = self._converter.parse(envelope)
199        if push is None:
200            return
201        self._apply(push)
202
203    def update_from_detail(self, detail: Q10CleanRecordDetail) -> None:
204        """Store a pushed clean-record map and path and notify listeners."""
205        if detail.map.kind is not Q10MapPacketKind.CLEAN_RECORD_DETAIL:
206            raise ValueError(f"Expected a Q10 clean-record detail packet, got {detail.map.kind.value}")
207        self.detail = detail
208        self.detail_path = list(detail.trace.points) if detail.trace else []
209        self._notify_update()
210
211    def _apply(self, push: CleanRecordPush) -> None:
212        """Merge or replace the records from ``push``, then sort newest-first and notify."""
213        if push.replace:
214            records = list(push.records)
215        else:
216            updated_ids = {record.record_id for record in push.records}
217            records = [record for record in self.records if record.record_id not in updated_ids]
218            records.extend(push.records)
219        records.sort(key=lambda record: record.start_time or 0, reverse=True)
220        self.records = records
221        self._notify_update()

Browse past cleaning runs and retrieve their parsed maps and historical paths.

Subscribe with add_update_listener to receive requested or unsolicited archives. Archive pushes contain no record ID, so the latest detail cannot be attributed to a particular selection.

CleanHistoryTrait(command: roborock.devices.traits.b01.q10.command.CommandTrait)
162    def __init__(self, command: CommandTrait) -> None:
163        CleanHistory.__init__(self)
164        UpdatableTrait.__init__(self, command, _LOGGER)
165        self._command = command
166        self._converter = CleanRecordConverter()

Initialize the update listener. The read-model is initialized by the subclass.

async def refresh(self) -> None:
168    async def refresh(self) -> None:
169        """Request the clean-record list from the device.
170
171        This sends the query and returns immediately; the records arrive
172        asynchronously on the device stream and populate :attr:`records` once
173        :meth:`update_from_dps` processes the ``dpCleanRecord`` push.
174        """
175        await self._command.send(
176            B01_Q10_DP.COMMON,
177            params={str(B01_Q10_DP.CLEAN_RECORD.code): {"op": "list"}},
178        )

Request the clean-record list from the device.

This sends the query and returns immediately; the records arrive asynchronously on the device stream and populate records once update_from_dps() processes the dpCleanRecord push.

async def refresh_detail( self, record: roborock.data.b01_q10.b01_q10_containers.Q10CleanRecord) -> None:
180    async def refresh_detail(self, record: Q10CleanRecord) -> None:
181        """Request the saved map and path for a past cleaning run.
182
183        Returns after sending; listeners receive the archive separately. The
184        push contains no record ID to correlate it with this selection.
185        """
186        if not record.raw or not record.map_len:
187            raise RoborockException("The Q10 clean record has no saved map detail")
188        await self._command.send(
189            B01_Q10_DP.COMMON,
190            {str(B01_Q10_DP.CLEAN_RECORD.code): {"op": "select", "id": record.raw}},
191        )

Request the saved map and path for a past cleaning run.

Returns after sending; listeners receive the archive separately. The push contains no record ID to correlate it with this selection.

def update_from_dps( self, decoded_dps: dict[roborock.data.b01_q10.b01_q10_code_mappings.B01_Q10_DP, typing.Any]) -> None:
193    def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None:
194        """Apply a ``dpCleanRecord`` push (a full list reply or a single notify)."""
195        envelope = decoded_dps.get(B01_Q10_DP.CLEAN_RECORD)
196        if not isinstance(envelope, dict):
197            return
198        push = self._converter.parse(envelope)
199        if push is None:
200            return
201        self._apply(push)

Apply a dpCleanRecord push (a full list reply or a single notify).

def update_from_detail( self, detail: roborock.map.b01_q10_map_parser.Q10CleanRecordDetail) -> None:
203    def update_from_detail(self, detail: Q10CleanRecordDetail) -> None:
204        """Store a pushed clean-record map and path and notify listeners."""
205        if detail.map.kind is not Q10MapPacketKind.CLEAN_RECORD_DETAIL:
206            raise ValueError(f"Expected a Q10 clean-record detail packet, got {detail.map.kind.value}")
207        self.detail = detail
208        self.detail_path = list(detail.trace.points) if detail.trace else []
209        self._notify_update()

Store a pushed clean-record map and path and notify listeners.

class ConsumableTrait(roborock.data.b01_q10.b01_q10_containers.Q10Consumable, roborock.devices.traits.b01.q10.common.UpdatableTrait):
14class ConsumableTrait(Q10Consumable, UpdatableTrait):
15    """Trait exposing remaining life of consumables (brushes, filter, sensors)."""
16
17    _CONVERTER = DpsDataConverter.from_dataclass(Q10Consumable)
18
19    def __init__(self) -> None:
20        """Initialize the consumable trait."""
21        Q10Consumable.__init__(self)
22        UpdatableTrait.__init__(self, command=None, logger=_LOGGER)

Trait exposing remaining life of consumables (brushes, filter, sensors).

ConsumableTrait()
19    def __init__(self) -> None:
20        """Initialize the consumable trait."""
21        Q10Consumable.__init__(self)
22        UpdatableTrait.__init__(self, command=None, logger=_LOGGER)

Initialize the consumable trait.

class DoNotDisturbTrait(roborock.data.b01_q10.b01_q10_containers.DoNotDisturb, roborock.devices.traits.b01.q10.common.UpdatableTrait):
16class DoNotDisturbTrait(DoNotDisturb, UpdatableTrait):
17    """Trait for reading and controlling Do Not Disturb on a Q10 device."""
18
19    _CONVERTER = DpsDataConverter.from_dataclass(DoNotDisturb)
20
21    def __init__(self, command: CommandTrait) -> None:
22        """Initialize the Do Not Disturb trait."""
23        DoNotDisturb.__init__(self)
24        UpdatableTrait.__init__(self, command, _LOGGER)
25
26    @property
27    def is_on(self) -> bool:
28        """Return whether Do Not Disturb is enabled."""
29        return bool(self.not_disturb)
30
31    async def enable(self) -> None:
32        """Enable Do Not Disturb."""
33        await self._write(B01_Q10_DP.NOT_DISTURB, 1)
34
35    async def disable(self) -> None:
36        """Disable Do Not Disturb."""
37        await self._write(B01_Q10_DP.NOT_DISTURB, 0)

Trait for reading and controlling Do Not Disturb on a Q10 device.

DoNotDisturbTrait(command: roborock.devices.traits.b01.q10.command.CommandTrait)
21    def __init__(self, command: CommandTrait) -> None:
22        """Initialize the Do Not Disturb trait."""
23        DoNotDisturb.__init__(self)
24        UpdatableTrait.__init__(self, command, _LOGGER)

Initialize the Do Not Disturb trait.

is_on: bool
26    @property
27    def is_on(self) -> bool:
28        """Return whether Do Not Disturb is enabled."""
29        return bool(self.not_disturb)

Return whether Do Not Disturb is enabled.

async def enable(self) -> None:
31    async def enable(self) -> None:
32        """Enable Do Not Disturb."""
33        await self._write(B01_Q10_DP.NOT_DISTURB, 1)

Enable Do Not Disturb.

async def disable(self) -> None:
35    async def disable(self) -> None:
36        """Disable Do Not Disturb."""
37        await self._write(B01_Q10_DP.NOT_DISTURB, 0)

Disable Do Not Disturb.

class DustCollectionTrait(roborock.data.b01_q10.b01_q10_containers.DustCollection, roborock.devices.traits.b01.q10.common.UpdatableTrait):
16class DustCollectionTrait(DustCollection, UpdatableTrait):
17    """Trait for reading and controlling automatic dust collection at the dock."""
18
19    _CONVERTER = DpsDataConverter.from_dataclass(DustCollection)
20
21    def __init__(self, command: CommandTrait) -> None:
22        """Initialize the dust collection trait."""
23        DustCollection.__init__(self)
24        UpdatableTrait.__init__(self, command, _LOGGER)
25
26    @property
27    def is_on(self) -> bool:
28        """Return whether automatic dust collection is enabled."""
29        return bool(self.dust_switch)
30
31    async def enable(self) -> None:
32        """Enable automatic dust collection at the dock."""
33        await self._write(B01_Q10_DP.DUST_SWITCH, 1)
34
35    async def disable(self) -> None:
36        """Disable automatic dust collection at the dock."""
37        await self._write(B01_Q10_DP.DUST_SWITCH, 0)
38
39    async def set_frequency(self, frequency: YXDeviceDustCollectionFrequency) -> None:
40        """Set how often the dock empties the bin.
41
42        The value is the interval in cleans, with ``DAILY`` (0) meaning daily.
43        """
44        await self._write(B01_Q10_DP.DUST_SETTING, frequency.code)

Trait for reading and controlling automatic dust collection at the dock.

DustCollectionTrait(command: roborock.devices.traits.b01.q10.command.CommandTrait)
21    def __init__(self, command: CommandTrait) -> None:
22        """Initialize the dust collection trait."""
23        DustCollection.__init__(self)
24        UpdatableTrait.__init__(self, command, _LOGGER)

Initialize the dust collection trait.

is_on: bool
26    @property
27    def is_on(self) -> bool:
28        """Return whether automatic dust collection is enabled."""
29        return bool(self.dust_switch)

Return whether automatic dust collection is enabled.

async def enable(self) -> None:
31    async def enable(self) -> None:
32        """Enable automatic dust collection at the dock."""
33        await self._write(B01_Q10_DP.DUST_SWITCH, 1)

Enable automatic dust collection at the dock.

async def disable(self) -> None:
35    async def disable(self) -> None:
36        """Disable automatic dust collection at the dock."""
37        await self._write(B01_Q10_DP.DUST_SWITCH, 0)

Disable automatic dust collection at the dock.

async def set_frequency( self, frequency: roborock.data.b01_q10.b01_q10_code_mappings.YXDeviceDustCollectionFrequency) -> None:
39    async def set_frequency(self, frequency: YXDeviceDustCollectionFrequency) -> None:
40        """Set how often the dock empties the bin.
41
42        The value is the interval in cleans, with ``DAILY`` (0) meaning daily.
43        """
44        await self._write(B01_Q10_DP.DUST_SETTING, frequency.code)

Set how often the dock empties the bin.

The value is the interval in cleans, with DAILY (0) meaning daily.

class MapContentTrait(roborock.devices.traits.common.TraitUpdateListener):
 90class MapContentTrait(TraitUpdateListener):
 91    """High-level composed Q10 map view.
 92
 93    The latest map and trace packets are combined with the injected
 94    :class:`MapDpsTrait` whenever a source changes. Current-map acquisition is
 95    independent of the saved-map list.
 96    """
 97
 98    def __init__(
 99        self,
100        map_dps: MapDpsTrait,
101        command: CommandTrait,
102        *,
103        map_parser_config: B01Q10MapParserConfig | None = None,
104    ) -> None:
105        TraitUpdateListener.__init__(self, logger=_LOGGER)
106        self._config = map_parser_config or B01Q10MapParserConfig()
107        self._map_dps = map_dps
108        self._command = command
109        self._map_packet: Q10MapPacket | None = None
110        self._trace_packet: Q10TracePacket | None = None
111        self._image_content: bytes | None = None
112        self._map_dps.add_update_listener(self._map_dps_updated)
113
114    async def refresh(self) -> None:
115        """Request a safe asynchronous current-map/status push.
116
117        Some ss07 firmware treats ``dpMultiMap op:get`` as an active
118        cleaning/relocation command. ``REQUEST_DPS`` is the device's read-only
119        current-map request and does not depend on a saved-map ID.
120        """
121        await self._command.send(B01_Q10_DP.REQUEST_DPS, params={})
122
123    @property
124    def image_content(self) -> bytes | None:
125        """The composed map PNG, if the latest map rendered successfully."""
126        return self._image_content
127
128    @property
129    def rooms(self) -> list[Q10Room]:
130        """Rooms reported by the device."""
131        return self._map_packet.rooms if self._map_packet else []
132
133    @property
134    def path(self) -> list[Q10Point]:
135        """Full path in the Q10 trace coordinate space used by the map renderer."""
136        return self._trace_packet.points if self._trace_packet else []
137
138    @property
139    def restricted_zones(self) -> tuple[Q10RestrictedZone, ...]:
140        """Restrictions reported by the device in common Roborock coordinates."""
141        restrictions: list[Q10RestrictedZone] = []
142        for zone in self._map_dps.overlays.zones:
143            if len(zone.vertices) != 4:
144                continue
145            vertices = (
146                _to_roborock_point(zone.vertices[0]),
147                _to_roborock_point(zone.vertices[1]),
148                _to_roborock_point(zone.vertices[2]),
149                _to_roborock_point(zone.vertices[3]),
150            )
151            restriction_type = Q10RestrictionType(zone.type)
152            restrictions.append(Q10RestrictedZone(restriction_type, vertices))
153        return tuple(restrictions)
154
155    @property
156    def virtual_walls(self) -> tuple[Q10VirtualWall, ...]:
157        """Virtual walls reported by the device in common Roborock coordinates."""
158        walls: list[Q10VirtualWall] = []
159        for wall in self._map_dps.overlays.virtual_walls:
160            if len(wall.vertices) != 2:
161                continue
162            walls.append(
163                Q10VirtualWall(
164                    start=_to_roborock_point(wall.vertices[0]),
165                    end=_to_roborock_point(wall.vertices[1]),
166                )
167            )
168        return tuple(walls)
169
170    async def set_restricted_zones(self, zones: Sequence[Q10RestrictedZone]) -> None:
171        """Replace the complete restricted-zone collection."""
172        payload = encode_restricted_zones(zones)
173        await self._command.send(
174            B01_Q10_DP.COMMON,
175            {str(B01_Q10_DP.RESTRICTED_ZONE.code): payload},
176        )
177
178    async def set_virtual_walls(self, walls: Sequence[Q10VirtualWall]) -> None:
179        """Replace the complete virtual-wall collection."""
180        payload = encode_virtual_walls(walls)
181        await self._command.send(
182            B01_Q10_DP.COMMON,
183            {str(B01_Q10_DP.VIRTUAL_WALL.code): payload},
184        )
185
186    async def set_room_name(self, room_id: int, name: str) -> None:
187        """Set the name of a room in the current map."""
188        if room_id not in {room.id for room in self.rooms}:
189            raise RoborockException(f"Unknown Q10 room ID: {room_id}")
190        payload = encode_room_name(room_id, name)
191        await self._command.send(
192            B01_Q10_DP.COMMON,
193            {str(B01_Q10_DP.RESET_ROOM_NAME.code): payload},
194        )
195
196    @property
197    def robot_position(self) -> Q10RoborockPoint | None:
198        """Current position in the common Roborock millimetre coordinate space."""
199        if self._trace_packet is None or (position := self._trace_packet.robot_position) is None:
200            return None
201        return position.to_roborock()
202
203    @property
204    def trace_sequence(self) -> int | None:
205        """Current cleaning-session sequence from the trace stream."""
206        return self._trace_packet.sequence if self._trace_packet else None
207
208    @property
209    def robot_heading(self) -> int | None:
210        """Current heading for orienting a robot marker on a caller-rendered map."""
211        return self._trace_packet.heading if self._trace_packet else None
212
213    @property
214    def current_room(self) -> Q10Room | None:
215        """Room inferred from the latest live robot position and segmented map.
216
217        Returns ``None`` when position or calibration is unavailable, the pose
218        lies outside the map, or nearby room labels are ambiguous.
219        """
220        if self._map_packet is None or self._trace_packet is None:
221            return None
222        return resolve_q10_current_room(self._map_packet, self._trace_packet)
223
224    def update_from_map_packet(self, packet: Q10MapPacket) -> None:
225        """Store a map-protocol update and render the latest sources."""
226        if packet.kind is not Q10MapPacketKind.CURRENT:
227            raise ValueError(f"Expected a current Q10 map packet, got {packet.kind.value}")
228        self._map_packet = packet
229        self._render()
230        self._notify_update()
231
232    def update_from_trace_packet(self, packet: Q10TracePacket) -> None:
233        """Store a trace-protocol update and render the latest sources."""
234        self._trace_packet = None if self._map_dps.robot_at_dock else packet
235        self._render()
236        self._notify_update()
237
238    def _map_dps_updated(self) -> None:
239        """Render after the low-level map DPS source changes."""
240        if self._map_dps.robot_at_dock:
241            # A completed path no longer identifies the current pose at dock.
242            self._trace_packet = None
243        if self._map_packet is None:
244            return
245        self._render()
246        self._notify_update()
247
248    def _render(self) -> None:
249        """Render the required map with the latest optional trace and overlays."""
250        if self._map_packet is None:
251            return
252        try:
253            self._image_content = render_q10_map(
254                self._map_packet,
255                self._trace_packet if not self._map_dps.robot_at_dock else None,
256                self._map_dps.overlays,
257                config=self._config,
258                robot_at_dock=self._map_dps.robot_at_dock,
259            )
260        except RoborockException as ex:
261            _LOGGER.debug("Failed to render Q10 map packet: %s", ex)
262            self._image_content = None
263
264    def as_dict(self, exclude: set[str] | None = None) -> dict[str, Any]:
265        """Return the trait data as a dictionary, excluding large binary data."""
266        exclude_set = exclude or set()
267        current_room = self.current_room
268        data = {
269            "rooms": [room.as_dict() for room in self.rooms],
270            "path": [point.as_dict() for point in self.path],
271            "robotPosition": (
272                {"x": position.x, "y": position.y} if (position := self.robot_position) is not None else None
273            ),
274            "robotHeading": self.robot_heading,
275            "currentRoom": current_room.as_dict() if current_room is not None else None,
276        }
277        for key in exclude_set:
278            data.pop(key, None)
279        return data

High-level composed Q10 map view.

The latest map and trace packets are combined with the injected MapDpsTrait whenever a source changes. Current-map acquisition is independent of the saved-map list.

MapContentTrait( map_dps: roborock.devices.traits.b01.q10.map.MapDpsTrait, command: roborock.devices.traits.b01.q10.command.CommandTrait, *, map_parser_config: roborock.map.b01_q10_map_parser.B01Q10MapParserConfig | None = None)
 98    def __init__(
 99        self,
100        map_dps: MapDpsTrait,
101        command: CommandTrait,
102        *,
103        map_parser_config: B01Q10MapParserConfig | None = None,
104    ) -> None:
105        TraitUpdateListener.__init__(self, logger=_LOGGER)
106        self._config = map_parser_config or B01Q10MapParserConfig()
107        self._map_dps = map_dps
108        self._command = command
109        self._map_packet: Q10MapPacket | None = None
110        self._trace_packet: Q10TracePacket | None = None
111        self._image_content: bytes | None = None
112        self._map_dps.add_update_listener(self._map_dps_updated)

Initialize the trait update listener.

async def refresh(self) -> None:
114    async def refresh(self) -> None:
115        """Request a safe asynchronous current-map/status push.
116
117        Some ss07 firmware treats ``dpMultiMap op:get`` as an active
118        cleaning/relocation command. ``REQUEST_DPS`` is the device's read-only
119        current-map request and does not depend on a saved-map ID.
120        """
121        await self._command.send(B01_Q10_DP.REQUEST_DPS, params={})

Request a safe asynchronous current-map/status push.

Some ss07 firmware treats dpMultiMap op:get as an active cleaning/relocation command. REQUEST_DPS is the device's read-only current-map request and does not depend on a saved-map ID.

image_content: bytes | None
123    @property
124    def image_content(self) -> bytes | None:
125        """The composed map PNG, if the latest map rendered successfully."""
126        return self._image_content

The composed map PNG, if the latest map rendered successfully.

rooms: list[roborock.map.b01_q10_map_parser.Q10Room]
128    @property
129    def rooms(self) -> list[Q10Room]:
130        """Rooms reported by the device."""
131        return self._map_packet.rooms if self._map_packet else []

Rooms reported by the device.

path: list[roborock.map.b01_q10_map_parser.Q10Point]
133    @property
134    def path(self) -> list[Q10Point]:
135        """Full path in the Q10 trace coordinate space used by the map renderer."""
136        return self._trace_packet.points if self._trace_packet else []

Full path in the Q10 trace coordinate space used by the map renderer.

restricted_zones: tuple[roborock.map.b01_q10_overlays.Q10RestrictedZone, ...]
138    @property
139    def restricted_zones(self) -> tuple[Q10RestrictedZone, ...]:
140        """Restrictions reported by the device in common Roborock coordinates."""
141        restrictions: list[Q10RestrictedZone] = []
142        for zone in self._map_dps.overlays.zones:
143            if len(zone.vertices) != 4:
144                continue
145            vertices = (
146                _to_roborock_point(zone.vertices[0]),
147                _to_roborock_point(zone.vertices[1]),
148                _to_roborock_point(zone.vertices[2]),
149                _to_roborock_point(zone.vertices[3]),
150            )
151            restriction_type = Q10RestrictionType(zone.type)
152            restrictions.append(Q10RestrictedZone(restriction_type, vertices))
153        return tuple(restrictions)

Restrictions reported by the device in common Roborock coordinates.

virtual_walls: tuple[roborock.map.b01_q10_overlays.Q10VirtualWall, ...]
155    @property
156    def virtual_walls(self) -> tuple[Q10VirtualWall, ...]:
157        """Virtual walls reported by the device in common Roborock coordinates."""
158        walls: list[Q10VirtualWall] = []
159        for wall in self._map_dps.overlays.virtual_walls:
160            if len(wall.vertices) != 2:
161                continue
162            walls.append(
163                Q10VirtualWall(
164                    start=_to_roborock_point(wall.vertices[0]),
165                    end=_to_roborock_point(wall.vertices[1]),
166                )
167            )
168        return tuple(walls)

Virtual walls reported by the device in common Roborock coordinates.

async def set_restricted_zones( self, zones: Sequence[roborock.map.b01_q10_overlays.Q10RestrictedZone]) -> None:
170    async def set_restricted_zones(self, zones: Sequence[Q10RestrictedZone]) -> None:
171        """Replace the complete restricted-zone collection."""
172        payload = encode_restricted_zones(zones)
173        await self._command.send(
174            B01_Q10_DP.COMMON,
175            {str(B01_Q10_DP.RESTRICTED_ZONE.code): payload},
176        )

Replace the complete restricted-zone collection.

async def set_virtual_walls( self, walls: Sequence[roborock.map.b01_q10_overlays.Q10VirtualWall]) -> None:
178    async def set_virtual_walls(self, walls: Sequence[Q10VirtualWall]) -> None:
179        """Replace the complete virtual-wall collection."""
180        payload = encode_virtual_walls(walls)
181        await self._command.send(
182            B01_Q10_DP.COMMON,
183            {str(B01_Q10_DP.VIRTUAL_WALL.code): payload},
184        )

Replace the complete virtual-wall collection.

async def set_room_name(self, room_id: int, name: str) -> None:
186    async def set_room_name(self, room_id: int, name: str) -> None:
187        """Set the name of a room in the current map."""
188        if room_id not in {room.id for room in self.rooms}:
189            raise RoborockException(f"Unknown Q10 room ID: {room_id}")
190        payload = encode_room_name(room_id, name)
191        await self._command.send(
192            B01_Q10_DP.COMMON,
193            {str(B01_Q10_DP.RESET_ROOM_NAME.code): payload},
194        )

Set the name of a room in the current map.

196    @property
197    def robot_position(self) -> Q10RoborockPoint | None:
198        """Current position in the common Roborock millimetre coordinate space."""
199        if self._trace_packet is None or (position := self._trace_packet.robot_position) is None:
200            return None
201        return position.to_roborock()

Current position in the common Roborock millimetre coordinate space.

trace_sequence: int | None
203    @property
204    def trace_sequence(self) -> int | None:
205        """Current cleaning-session sequence from the trace stream."""
206        return self._trace_packet.sequence if self._trace_packet else None

Current cleaning-session sequence from the trace stream.

robot_heading: int | None
208    @property
209    def robot_heading(self) -> int | None:
210        """Current heading for orienting a robot marker on a caller-rendered map."""
211        return self._trace_packet.heading if self._trace_packet else None

Current heading for orienting a robot marker on a caller-rendered map.

current_room: roborock.map.b01_q10_map_parser.Q10Room | None
213    @property
214    def current_room(self) -> Q10Room | None:
215        """Room inferred from the latest live robot position and segmented map.
216
217        Returns ``None`` when position or calibration is unavailable, the pose
218        lies outside the map, or nearby room labels are ambiguous.
219        """
220        if self._map_packet is None or self._trace_packet is None:
221            return None
222        return resolve_q10_current_room(self._map_packet, self._trace_packet)

Room inferred from the latest live robot position and segmented map.

Returns None when position or calibration is unavailable, the pose lies outside the map, or nearby room labels are ambiguous.

def update_from_map_packet(self, packet: roborock.map.b01_q10_map_parser.Q10MapPacket) -> None:
224    def update_from_map_packet(self, packet: Q10MapPacket) -> None:
225        """Store a map-protocol update and render the latest sources."""
226        if packet.kind is not Q10MapPacketKind.CURRENT:
227            raise ValueError(f"Expected a current Q10 map packet, got {packet.kind.value}")
228        self._map_packet = packet
229        self._render()
230        self._notify_update()

Store a map-protocol update and render the latest sources.

def update_from_trace_packet(self, packet: roborock.map.b01_q10_map_parser.Q10TracePacket) -> None:
232    def update_from_trace_packet(self, packet: Q10TracePacket) -> None:
233        """Store a trace-protocol update and render the latest sources."""
234        self._trace_packet = None if self._map_dps.robot_at_dock else packet
235        self._render()
236        self._notify_update()

Store a trace-protocol update and render the latest sources.

def as_dict(self, exclude: set[str] | None = None) -> dict[str, typing.Any]:
264    def as_dict(self, exclude: set[str] | None = None) -> dict[str, Any]:
265        """Return the trait data as a dictionary, excluding large binary data."""
266        exclude_set = exclude or set()
267        current_room = self.current_room
268        data = {
269            "rooms": [room.as_dict() for room in self.rooms],
270            "path": [point.as_dict() for point in self.path],
271            "robotPosition": (
272                {"x": position.x, "y": position.y} if (position := self.robot_position) is not None else None
273            ),
274            "robotHeading": self.robot_heading,
275            "currentRoom": current_room.as_dict() if current_room is not None else None,
276        }
277        for key in exclude_set:
278            data.pop(key, None)
279        return data

Return the trait data as a dictionary, excluding large binary data.

class MapsTrait(roborock.devices.traits.b01.q10.maps.Maps, roborock.devices.traits.b01.q10.common.UpdatableTrait):
 51class MapsTrait(Maps, UpdatableTrait):
 52    """Browse saved maps and retrieve their parsed previews independently of the live map.
 53
 54    Preview pushes identify their map, but carry no request correlation ID.
 55    Consumers receive them through ``add_update_listener``.
 56    """
 57
 58    _CONVERTER = DpsDataConverter.from_dataclass(Maps)
 59    _command: CommandTrait
 60
 61    def __init__(self, command: CommandTrait) -> None:
 62        """Initialize the saved-map list trait."""
 63        Maps.__init__(self)
 64        UpdatableTrait.__init__(self, command, _LOGGER)
 65        self._command = command
 66
 67    async def refresh(self) -> None:
 68        """Request a new saved-map list from the device."""
 69        await self._command.send(
 70            B01_Q10_DP.COMMON,
 71            {str(B01_Q10_DP.MULTI_MAP.code): {"op": "list"}},
 72        )
 73
 74    async def set_current_map(self, map_id: str) -> None:
 75        """Request a saved map without optimistically changing cached state."""
 76        if map_id not in {map_info.id for map_info in self.map_list}:
 77            raise RoborockException(f"Unknown Q10 saved-map ID: {map_id}")
 78        await self._command.send(
 79            B01_Q10_DP.COMMON,
 80            {
 81                str(B01_Q10_DP.MULTI_MAP.code): {
 82                    "op": "apply",
 83                    "id": map_id,
 84                }
 85            },
 86        )
 87
 88    async def refresh_detail(self, map_id: str | None = None) -> None:
 89        """Request a read-only preview for one saved map.
 90
 91        Returns after publishing. Subscribe with ``add_update_listener`` to
 92        receive previews separately from the live map. ``detail_map_id`` is
 93        read from each pushed packet; it is not a request correlation ID.
 94        """
 95        if map_id is None:
 96            map_id = self.current_map_id
 97        if map_id is None:
 98            raise RoborockException("Cannot request Q10 saved-map detail before the map list is available")
 99        if map_id not in {map_info.id for map_info in self.map_list}:
100            raise RoborockException(f"Unknown Q10 saved-map ID: {map_id}")
101        await self._command.send(
102            B01_Q10_DP.COMMON,
103            {str(B01_Q10_DP.MULTI_MAP.code): {"op": "select", "id": map_id}},
104        )
105
106    def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None:
107        """Store a successful saved-map list response."""
108        response = decoded_dps.get(B01_Q10_DP.MULTI_MAP)
109        # DP 61 also carries map-content acknowledgements. Ignore them so they
110        # cannot replace a usable map list with an unrelated response.
111        if not isinstance(response, dict) or response.get("op") != "list" or response.get("result") != 1:
112            return
113        super().update_from_dps(decoded_dps)
114
115    def update_from_map_packet(self, packet: Q10MapPacket) -> None:
116        """Store a pushed saved-map detail packet and notify listeners."""
117        if packet.kind is not Q10MapPacketKind.SAVED_MAP_DETAIL:
118            raise ValueError(f"Expected a Q10 saved-map detail packet, got {packet.kind.value}")
119        self.detail_map_id = str(packet.map_id)
120        self.detail_packet = packet
121        self._notify_update()

Browse saved maps and retrieve their parsed previews independently of the live map.

Preview pushes identify their map, but carry no request correlation ID. Consumers receive them through add_update_listener.

MapsTrait(command: roborock.devices.traits.b01.q10.command.CommandTrait)
61    def __init__(self, command: CommandTrait) -> None:
62        """Initialize the saved-map list trait."""
63        Maps.__init__(self)
64        UpdatableTrait.__init__(self, command, _LOGGER)
65        self._command = command

Initialize the saved-map list trait.

async def refresh(self) -> None:
67    async def refresh(self) -> None:
68        """Request a new saved-map list from the device."""
69        await self._command.send(
70            B01_Q10_DP.COMMON,
71            {str(B01_Q10_DP.MULTI_MAP.code): {"op": "list"}},
72        )

Request a new saved-map list from the device.

async def set_current_map(self, map_id: str) -> None:
74    async def set_current_map(self, map_id: str) -> None:
75        """Request a saved map without optimistically changing cached state."""
76        if map_id not in {map_info.id for map_info in self.map_list}:
77            raise RoborockException(f"Unknown Q10 saved-map ID: {map_id}")
78        await self._command.send(
79            B01_Q10_DP.COMMON,
80            {
81                str(B01_Q10_DP.MULTI_MAP.code): {
82                    "op": "apply",
83                    "id": map_id,
84                }
85            },
86        )

Request a saved map without optimistically changing cached state.

async def refresh_detail(self, map_id: str | None = None) -> None:
 88    async def refresh_detail(self, map_id: str | None = None) -> None:
 89        """Request a read-only preview for one saved map.
 90
 91        Returns after publishing. Subscribe with ``add_update_listener`` to
 92        receive previews separately from the live map. ``detail_map_id`` is
 93        read from each pushed packet; it is not a request correlation ID.
 94        """
 95        if map_id is None:
 96            map_id = self.current_map_id
 97        if map_id is None:
 98            raise RoborockException("Cannot request Q10 saved-map detail before the map list is available")
 99        if map_id not in {map_info.id for map_info in self.map_list}:
100            raise RoborockException(f"Unknown Q10 saved-map ID: {map_id}")
101        await self._command.send(
102            B01_Q10_DP.COMMON,
103            {str(B01_Q10_DP.MULTI_MAP.code): {"op": "select", "id": map_id}},
104        )

Request a read-only preview for one saved map.

Returns after publishing. Subscribe with add_update_listener to receive previews separately from the live map. detail_map_id is read from each pushed packet; it is not a request correlation ID.

def update_from_dps( self, decoded_dps: dict[roborock.data.b01_q10.b01_q10_code_mappings.B01_Q10_DP, typing.Any]) -> None:
106    def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None:
107        """Store a successful saved-map list response."""
108        response = decoded_dps.get(B01_Q10_DP.MULTI_MAP)
109        # DP 61 also carries map-content acknowledgements. Ignore them so they
110        # cannot replace a usable map list with an unrelated response.
111        if not isinstance(response, dict) or response.get("op") != "list" or response.get("result") != 1:
112            return
113        super().update_from_dps(decoded_dps)

Store a successful saved-map list response.

def update_from_map_packet(self, packet: roborock.map.b01_q10_map_parser.Q10MapPacket) -> None:
115    def update_from_map_packet(self, packet: Q10MapPacket) -> None:
116        """Store a pushed saved-map detail packet and notify listeners."""
117        if packet.kind is not Q10MapPacketKind.SAVED_MAP_DETAIL:
118            raise ValueError(f"Expected a Q10 saved-map detail packet, got {packet.kind.value}")
119        self.detail_map_id = str(packet.map_id)
120        self.detail_packet = packet
121        self._notify_update()

Store a pushed saved-map detail packet and notify listeners.

class NetworkInfoTrait(roborock.data.b01_q10.b01_q10_containers.Q10NetworkInfo, roborock.devices.traits.b01.q10.common.UpdatableTrait):
14class NetworkInfoTrait(Q10NetworkInfo, UpdatableTrait):
15    """Trait exposing the device's network information (read-only)."""
16
17    _CONVERTER = DpsDataConverter.from_dataclass(Q10NetworkInfo)
18
19    def __init__(self) -> None:
20        """Initialize the network info trait."""
21        Q10NetworkInfo.__init__(self)
22        UpdatableTrait.__init__(self, command=None, logger=_LOGGER)

Trait exposing the device's network information (read-only).

NetworkInfoTrait()
19    def __init__(self) -> None:
20        """Initialize the network info trait."""
21        Q10NetworkInfo.__init__(self)
22        UpdatableTrait.__init__(self, command=None, logger=_LOGGER)

Initialize the network info trait.

class SoundVolumeTrait(roborock.data.b01_q10.b01_q10_containers.SoundVolume, roborock.devices.traits.b01.q10.common.UpdatableTrait):
16class SoundVolumeTrait(SoundVolume, UpdatableTrait):
17    """Trait for reading and setting the speaker volume of a Q10 device."""
18
19    _CONVERTER = DpsDataConverter.from_dataclass(SoundVolume)
20
21    def __init__(self, command: CommandTrait) -> None:
22        """Initialize the volume trait."""
23        SoundVolume.__init__(self)
24        UpdatableTrait.__init__(self, command, _LOGGER)
25
26    async def set_volume(self, volume: int) -> None:
27        """Set the speaker volume (0-100)."""
28        if not 0 <= volume <= 100:
29            raise ValueError("volume must be between 0 and 100")
30        await self._write(B01_Q10_DP.VOLUME, volume)

Trait for reading and setting the speaker volume of a Q10 device.

SoundVolumeTrait(command: roborock.devices.traits.b01.q10.command.CommandTrait)
21    def __init__(self, command: CommandTrait) -> None:
22        """Initialize the volume trait."""
23        SoundVolume.__init__(self)
24        UpdatableTrait.__init__(self, command, _LOGGER)

Initialize the volume trait.

async def set_volume(self, volume: int) -> None:
26    async def set_volume(self, volume: int) -> None:
27        """Set the speaker volume (0-100)."""
28        if not 0 <= volume <= 100:
29            raise ValueError("volume must be between 0 and 100")
30        await self._write(B01_Q10_DP.VOLUME, volume)

Set the speaker volume (0-100).

class StatusTrait(roborock.data.b01_q10.b01_q10_containers.Q10Status, roborock.devices.traits.b01.q10.common.UpdatableTrait):
14class StatusTrait(Q10Status, UpdatableTrait):
15    """Trait for managing the core status of Q10 Roborock devices.
16
17    This is a thin wrapper around Q10Status that provides the Trait interface.
18    The current values reflect the most recently received data from the device.
19    New values can be requested through the `Q10PropertiesApi`'s `refresh` method.
20    """
21
22    _CONVERTER = DpsDataConverter.from_dataclass(Q10Status)
23
24    def __init__(self) -> None:
25        """Initialize the status trait."""
26        Q10Status.__init__(self)
27        UpdatableTrait.__init__(self, command=None, logger=_LOGGER)

Trait for managing the core status of Q10 Roborock devices.

This is a thin wrapper around Q10Status that provides the Trait interface. The current values reflect the most recently received data from the device. New values can be requested through the Q10PropertiesApi's refresh method.

StatusTrait()
24    def __init__(self) -> None:
25        """Initialize the status trait."""
26        Q10Status.__init__(self)
27        UpdatableTrait.__init__(self, command=None, logger=_LOGGER)

Initialize the status trait.