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 Q10MapPacket, Q10TracePacket 12from roborock.protocols.b01_q10_protocol import Q10DpsUpdate, Q10Message 13 14from .button_light import ButtonLightTrait 15from .child_lock import ChildLockTrait 16from .clean_history import CleanHistoryTrait 17from .command import CommandTrait 18from .consumable import ConsumableTrait 19from .do_not_disturb import DoNotDisturbTrait 20from .dust_collection import DustCollectionTrait 21from .map import MapContentTrait, MapDpsTrait 22from .maps import MapsTrait 23from .network_info import NetworkInfoTrait 24from .remote import RemoteTrait 25from .status import StatusTrait 26from .vacuum import VacuumTrait 27from .volume import SoundVolumeTrait 28 29__all__ = [ 30 "Q10PropertiesApi", 31 "ButtonLightTrait", 32 "ChildLockTrait", 33 "CleanHistoryTrait", 34 "ConsumableTrait", 35 "DoNotDisturbTrait", 36 "DustCollectionTrait", 37 "MapContentTrait", 38 "MapsTrait", 39 "NetworkInfoTrait", 40 "SoundVolumeTrait", 41 "StatusTrait", 42] 43 44_LOGGER = logging.getLogger(__name__) 45 46 47class Q10PropertiesApi(Trait): 48 """API for interacting with B01 devices.""" 49 50 command: CommandTrait 51 """Trait for sending commands to Q10 devices.""" 52 53 status: StatusTrait 54 """Trait for managing the core status of Q10 devices.""" 55 56 vacuum: VacuumTrait 57 """Trait for sending vacuum related commands to Q10 devices.""" 58 59 remote: RemoteTrait 60 """Trait for sending remote control related commands to Q10 devices.""" 61 62 volume: SoundVolumeTrait 63 """Trait for reading / setting the speaker volume.""" 64 65 child_lock: ChildLockTrait 66 """Trait for reading / controlling the child lock.""" 67 68 do_not_disturb: DoNotDisturbTrait 69 """Trait for reading / controlling Do Not Disturb.""" 70 71 dust_collection: DustCollectionTrait 72 """Trait for reading / controlling dock auto-empty (dust collection).""" 73 74 button_light: ButtonLightTrait 75 """Trait for controlling the indicator / button light (LED).""" 76 77 network_info: NetworkInfoTrait 78 """Trait exposing the device's network information.""" 79 80 consumable: ConsumableTrait 81 """Trait exposing remaining life of consumables.""" 82 83 map: MapContentTrait 84 """Composed map image plus caller-facing map and trace data.""" 85 86 maps: MapsTrait 87 """Saved-map list metadata.""" 88 89 _map_dps: MapDpsTrait 90 """Private source of restricted zones and virtual walls received through DPS.""" 91 92 clean_history: CleanHistoryTrait 93 """Trait for fetching the device clean-record history (``dpCleanRecord``).""" 94 95 def __init__(self, channel: B01Q10Channel) -> None: 96 """Initialize the B01Props API.""" 97 self._channel = channel 98 self.command = CommandTrait(channel) 99 self.remote = RemoteTrait(self.command) 100 self.status = StatusTrait() 101 self.volume = SoundVolumeTrait(self.command) 102 self.child_lock = ChildLockTrait(self.command) 103 self.do_not_disturb = DoNotDisturbTrait(self.command) 104 self.dust_collection = DustCollectionTrait(self.command) 105 self.button_light = ButtonLightTrait(self.command) 106 self.network_info = NetworkInfoTrait() 107 self.consumable = ConsumableTrait() 108 self._map_dps = MapDpsTrait() 109 self.maps = MapsTrait(self.command) 110 self.map = MapContentTrait(self._map_dps, self.maps, self.command) 111 self.vacuum = VacuumTrait(self.command, self.status, self.map) 112 self.clean_history = CleanHistoryTrait(self.command) 113 # Read-model traits updated from the device's DPS push stream. 114 self._updatable_traits = [ 115 self.status, 116 self.volume, 117 self.child_lock, 118 self.do_not_disturb, 119 self.dust_collection, 120 self.network_info, 121 self.consumable, 122 self.clean_history, 123 self._map_dps, 124 self.maps, 125 ] 126 self._subscribe_task: asyncio.Task[None] | None = None 127 128 async def start(self) -> None: 129 """Start any necessary subscriptions for the trait.""" 130 self._subscribe_task = asyncio.create_task(self._subscribe_loop()) 131 132 async def close(self) -> None: 133 """Close any resources held by the trait.""" 134 await self.vacuum.close() 135 if self._subscribe_task is not None: 136 self._subscribe_task.cancel() 137 try: 138 await self._subscribe_task 139 except asyncio.CancelledError: 140 pass # ignore cancellation errors 141 self._subscribe_task = None 142 143 async def refresh(self) -> None: 144 """Refresh all traits.""" 145 # Sending REQUEST_DPS causes the device to publish its ordinary status 146 # values. Map-list and map-content refreshes have separate schedules. 147 await self.command.send(B01_Q10_DP.REQUEST_DPS, params={}) 148 149 async def _subscribe_loop(self) -> None: 150 """Persistent loop dispatching decoded messages to the read-model traits.""" 151 async for message in self._channel.subscribe_stream(): 152 self._handle_message(message) 153 154 def _handle_message(self, message: Q10Message) -> None: 155 """Route a single decoded message to the trait responsible for it. 156 157 Map and trace packets arrive as protocol-301 ``MAP_RESPONSE`` pushes. 158 Map-list DPS responses and other DPS updates feed the read-model traits. 159 """ 160 if isinstance(message, Q10MapPacket): 161 self.map.update_from_map_packet(message) 162 elif isinstance(message, Q10TracePacket): 163 self.map.update_from_trace_packet(message) 164 elif isinstance(message, Q10DpsUpdate): 165 _LOGGER.debug("Received Q10 status update: %s", message.dps) 166 # Notify all read-model traits about the new message; each trait 167 # only updates the fields that it is responsible for. 168 for trait in self._updatable_traits: 169 trait.update_from_dps(message.dps) 170 171 def as_dict(self) -> dict[str, Any]: 172 """Return the trait data as a dictionary.""" 173 result: dict[str, Any] = {} 174 for name, value in self.__dict__.items(): 175 if isinstance(value, RoborockBase) and not name.startswith("_"): 176 result[name] = value.as_dict() 177 if hasattr(self, "map") and hasattr(self.map, "as_dict"): 178 result["map"] = self.map.as_dict() 179 return result 180 181 182def create(channel: B01Q10Channel) -> Q10PropertiesApi: 183 """Create traits for B01 devices.""" 184 return Q10PropertiesApi(channel)
48class Q10PropertiesApi(Trait): 49 """API for interacting with B01 devices.""" 50 51 command: CommandTrait 52 """Trait for sending commands to Q10 devices.""" 53 54 status: StatusTrait 55 """Trait for managing the core status of Q10 devices.""" 56 57 vacuum: VacuumTrait 58 """Trait for sending vacuum related commands to Q10 devices.""" 59 60 remote: RemoteTrait 61 """Trait for sending remote control related commands to Q10 devices.""" 62 63 volume: SoundVolumeTrait 64 """Trait for reading / setting the speaker volume.""" 65 66 child_lock: ChildLockTrait 67 """Trait for reading / controlling the child lock.""" 68 69 do_not_disturb: DoNotDisturbTrait 70 """Trait for reading / controlling Do Not Disturb.""" 71 72 dust_collection: DustCollectionTrait 73 """Trait for reading / controlling dock auto-empty (dust collection).""" 74 75 button_light: ButtonLightTrait 76 """Trait for controlling the indicator / button light (LED).""" 77 78 network_info: NetworkInfoTrait 79 """Trait exposing the device's network information.""" 80 81 consumable: ConsumableTrait 82 """Trait exposing remaining life of consumables.""" 83 84 map: MapContentTrait 85 """Composed map image plus caller-facing map and trace data.""" 86 87 maps: MapsTrait 88 """Saved-map list metadata.""" 89 90 _map_dps: MapDpsTrait 91 """Private source of restricted zones and virtual walls received through DPS.""" 92 93 clean_history: CleanHistoryTrait 94 """Trait for fetching the device clean-record history (``dpCleanRecord``).""" 95 96 def __init__(self, channel: B01Q10Channel) -> None: 97 """Initialize the B01Props API.""" 98 self._channel = channel 99 self.command = CommandTrait(channel) 100 self.remote = RemoteTrait(self.command) 101 self.status = StatusTrait() 102 self.volume = SoundVolumeTrait(self.command) 103 self.child_lock = ChildLockTrait(self.command) 104 self.do_not_disturb = DoNotDisturbTrait(self.command) 105 self.dust_collection = DustCollectionTrait(self.command) 106 self.button_light = ButtonLightTrait(self.command) 107 self.network_info = NetworkInfoTrait() 108 self.consumable = ConsumableTrait() 109 self._map_dps = MapDpsTrait() 110 self.maps = MapsTrait(self.command) 111 self.map = MapContentTrait(self._map_dps, self.maps, self.command) 112 self.vacuum = VacuumTrait(self.command, self.status, self.map) 113 self.clean_history = CleanHistoryTrait(self.command) 114 # Read-model traits updated from the device's DPS push stream. 115 self._updatable_traits = [ 116 self.status, 117 self.volume, 118 self.child_lock, 119 self.do_not_disturb, 120 self.dust_collection, 121 self.network_info, 122 self.consumable, 123 self.clean_history, 124 self._map_dps, 125 self.maps, 126 ] 127 self._subscribe_task: asyncio.Task[None] | None = None 128 129 async def start(self) -> None: 130 """Start any necessary subscriptions for the trait.""" 131 self._subscribe_task = asyncio.create_task(self._subscribe_loop()) 132 133 async def close(self) -> None: 134 """Close any resources held by the trait.""" 135 await self.vacuum.close() 136 if self._subscribe_task is not None: 137 self._subscribe_task.cancel() 138 try: 139 await self._subscribe_task 140 except asyncio.CancelledError: 141 pass # ignore cancellation errors 142 self._subscribe_task = None 143 144 async def refresh(self) -> None: 145 """Refresh all traits.""" 146 # Sending REQUEST_DPS causes the device to publish its ordinary status 147 # values. Map-list and map-content refreshes have separate schedules. 148 await self.command.send(B01_Q10_DP.REQUEST_DPS, params={}) 149 150 async def _subscribe_loop(self) -> None: 151 """Persistent loop dispatching decoded messages to the read-model traits.""" 152 async for message in self._channel.subscribe_stream(): 153 self._handle_message(message) 154 155 def _handle_message(self, message: Q10Message) -> None: 156 """Route a single decoded message to the trait responsible for it. 157 158 Map and trace packets arrive as protocol-301 ``MAP_RESPONSE`` pushes. 159 Map-list DPS responses and other DPS updates feed the read-model traits. 160 """ 161 if isinstance(message, Q10MapPacket): 162 self.map.update_from_map_packet(message) 163 elif isinstance(message, Q10TracePacket): 164 self.map.update_from_trace_packet(message) 165 elif isinstance(message, Q10DpsUpdate): 166 _LOGGER.debug("Received Q10 status update: %s", message.dps) 167 # Notify all read-model traits about the new message; each trait 168 # only updates the fields that it is responsible for. 169 for trait in self._updatable_traits: 170 trait.update_from_dps(message.dps) 171 172 def as_dict(self) -> dict[str, Any]: 173 """Return the trait data as a dictionary.""" 174 result: dict[str, Any] = {} 175 for name, value in self.__dict__.items(): 176 if isinstance(value, RoborockBase) and not name.startswith("_"): 177 result[name] = value.as_dict() 178 if hasattr(self, "map") and hasattr(self.map, "as_dict"): 179 result["map"] = self.map.as_dict() 180 return result
API for interacting with B01 devices.
96 def __init__(self, channel: B01Q10Channel) -> None: 97 """Initialize the B01Props API.""" 98 self._channel = channel 99 self.command = CommandTrait(channel) 100 self.remote = RemoteTrait(self.command) 101 self.status = StatusTrait() 102 self.volume = SoundVolumeTrait(self.command) 103 self.child_lock = ChildLockTrait(self.command) 104 self.do_not_disturb = DoNotDisturbTrait(self.command) 105 self.dust_collection = DustCollectionTrait(self.command) 106 self.button_light = ButtonLightTrait(self.command) 107 self.network_info = NetworkInfoTrait() 108 self.consumable = ConsumableTrait() 109 self._map_dps = MapDpsTrait() 110 self.maps = MapsTrait(self.command) 111 self.map = MapContentTrait(self._map_dps, self.maps, self.command) 112 self.vacuum = VacuumTrait(self.command, self.status, self.map) 113 self.clean_history = CleanHistoryTrait(self.command) 114 # Read-model traits updated from the device's DPS push stream. 115 self._updatable_traits = [ 116 self.status, 117 self.volume, 118 self.child_lock, 119 self.do_not_disturb, 120 self.dust_collection, 121 self.network_info, 122 self.consumable, 123 self.clean_history, 124 self._map_dps, 125 self.maps, 126 ] 127 self._subscribe_task: asyncio.Task[None] | None = None
Initialize the B01Props API.
Trait for sending commands to Q10 devices.
Trait for sending vacuum related commands to Q10 devices.
Trait for sending remote control related commands to Q10 devices.
Trait for reading / controlling dock auto-empty (dust collection).
Trait for fetching the device clean-record history (dpCleanRecord).
129 async def start(self) -> None: 130 """Start any necessary subscriptions for the trait.""" 131 self._subscribe_task = asyncio.create_task(self._subscribe_loop())
Start any necessary subscriptions for the trait.
133 async def close(self) -> None: 134 """Close any resources held by the trait.""" 135 await self.vacuum.close() 136 if self._subscribe_task is not None: 137 self._subscribe_task.cancel() 138 try: 139 await self._subscribe_task 140 except asyncio.CancelledError: 141 pass # ignore cancellation errors 142 self._subscribe_task = None
Close any resources held by the trait.
144 async def refresh(self) -> None: 145 """Refresh all traits.""" 146 # Sending REQUEST_DPS causes the device to publish its ordinary status 147 # values. Map-list and map-content refreshes have separate schedules. 148 await self.command.send(B01_Q10_DP.REQUEST_DPS, params={})
Refresh all traits.
172 def as_dict(self) -> dict[str, Any]: 173 """Return the trait data as a dictionary.""" 174 result: dict[str, Any] = {} 175 for name, value in self.__dict__.items(): 176 if isinstance(value, RoborockBase) and not name.startswith("_"): 177 result[name] = value.as_dict() 178 if hasattr(self, "map") and hasattr(self.map, "as_dict"): 179 result["map"] = self.map.as_dict() 180 return result
Return the trait data as a dictionary.
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).
16 def __init__(self, command: CommandTrait) -> None: 17 """Initialize the button light trait.""" 18 self._command = command
Initialize the button light trait.
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.
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.
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.
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.
111class CleanHistoryTrait(UpdatableTrait): 112 """Access to the Q10 clean-record history (``dpCleanRecord``, DP 52). 113 114 A read-model trait updated from the DPS stream like the others, but it overrides 115 :meth:`update_from_dps` because the payload is a structured push (a record list, 116 or a single ``op:"notify"`` record) rather than a flat data-point-to-field map. 117 """ 118 119 def __init__(self, command: CommandTrait) -> None: 120 """Initialize the clean history trait.""" 121 UpdatableTrait.__init__(self, command, _LOGGER) 122 self._converter = CleanRecordConverter() 123 self.records: list[Q10CleanRecord] = [] 124 """Decoded clean records, most recent first.""" 125 126 @property 127 def last_record(self) -> Q10CleanRecord | None: 128 """The most recent clean record, or ``None`` if there are none.""" 129 return self.records[0] if self.records else None 130 131 async def refresh(self) -> None: 132 """Request the clean-record list from the device. 133 134 This sends the query and returns immediately; the records arrive 135 asynchronously on the device stream and populate :attr:`records` once 136 :meth:`update_from_dps` processes the ``dpCleanRecord`` push. 137 """ 138 if self._command is None: 139 raise ValueError("Trait is read-only; no command channel was provided") 140 await self._command.send( 141 B01_Q10_DP.COMMON, 142 params={str(B01_Q10_DP.CLEAN_RECORD.code): {"op": "list"}}, 143 ) 144 145 def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None: 146 """Apply a ``dpCleanRecord`` push (a full list reply or a single notify).""" 147 envelope = decoded_dps.get(B01_Q10_DP.CLEAN_RECORD) 148 if not isinstance(envelope, dict): 149 return 150 push = self._converter.parse(envelope) 151 if push is None: 152 return 153 self._apply(push) 154 155 def _apply(self, push: CleanRecordPush) -> None: 156 """Merge or replace the records from ``push``, then sort newest-first and notify.""" 157 if push.replace: 158 records = list(push.records) 159 else: 160 updated_ids = {record.record_id for record in push.records} 161 records = [record for record in self.records if record.record_id not in updated_ids] 162 records.extend(push.records) 163 records.sort(key=lambda record: record.start_time or 0, reverse=True) 164 self.records = records 165 self._notify_update()
Access to the Q10 clean-record history (dpCleanRecord, DP 52).
A read-model trait updated from the DPS stream like the others, but it overrides
update_from_dps() because the payload is a structured push (a record list,
or a single op:"notify" record) rather than a flat data-point-to-field map.
119 def __init__(self, command: CommandTrait) -> None: 120 """Initialize the clean history trait.""" 121 UpdatableTrait.__init__(self, command, _LOGGER) 122 self._converter = CleanRecordConverter() 123 self.records: list[Q10CleanRecord] = [] 124 """Decoded clean records, most recent first."""
Initialize the clean history trait.
Decoded clean records, most recent first.
126 @property 127 def last_record(self) -> Q10CleanRecord | None: 128 """The most recent clean record, or ``None`` if there are none.""" 129 return self.records[0] if self.records else None
The most recent clean record, or None if there are none.
131 async def refresh(self) -> None: 132 """Request the clean-record list from the device. 133 134 This sends the query and returns immediately; the records arrive 135 asynchronously on the device stream and populate :attr:`records` once 136 :meth:`update_from_dps` processes the ``dpCleanRecord`` push. 137 """ 138 if self._command is None: 139 raise ValueError("Trait is read-only; no command channel was provided") 140 await self._command.send( 141 B01_Q10_DP.COMMON, 142 params={str(B01_Q10_DP.CLEAN_RECORD.code): {"op": "list"}}, 143 )
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.
145 def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None: 146 """Apply a ``dpCleanRecord`` push (a full list reply or a single notify).""" 147 envelope = decoded_dps.get(B01_Q10_DP.CLEAN_RECORD) 148 if not isinstance(envelope, dict): 149 return 150 push = self._converter.parse(envelope) 151 if push is None: 152 return 153 self._apply(push)
Apply a dpCleanRecord push (a full list reply or a single notify).
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
84class MapContentTrait(TraitUpdateListener): 85 """High-level composed Q10 map view. 86 87 The latest map and trace packets are combined with the injected 88 :class:`MapDpsTrait` whenever a source changes. The 89 :class:`MapsTrait` supplies a stored ID only when this trait requests 90 content. 91 """ 92 93 def __init__( 94 self, 95 map_dps: MapDpsTrait, 96 maps: MapsTrait, 97 command: CommandTrait, 98 *, 99 map_parser_config: B01Q10MapParserConfig | None = None, 100 ) -> None: 101 TraitUpdateListener.__init__(self, logger=_LOGGER) 102 self._config = map_parser_config or B01Q10MapParserConfig() 103 self._map_dps = map_dps 104 self._maps = maps 105 self._command = command 106 self._map_packet: Q10MapPacket | None = None 107 self._trace_packet: Q10TracePacket | None = None 108 self._image_content: bytes | None = None 109 self._map_dps.add_update_listener(self._map_dps_updated) 110 111 async def refresh(self) -> None: 112 """Request a safe asynchronous current-map/status push. 113 114 Some ss07 firmware treats ``dpMultiMap op:get`` as an active 115 cleaning/relocation command. ``REQUEST_DPS`` is the device's read-only 116 current-map request and does not depend on a saved-map ID. 117 """ 118 await self._command.send(B01_Q10_DP.REQUEST_DPS, params={}) 119 120 @property 121 def image_content(self) -> bytes | None: 122 """The composed map PNG, if the latest map rendered successfully.""" 123 return self._image_content 124 125 @property 126 def rooms(self) -> list[Q10Room]: 127 """Rooms reported by the device.""" 128 return self._map_packet.rooms if self._map_packet else [] 129 130 @property 131 def path(self) -> list[Q10Point]: 132 """Full path in the Q10 trace coordinate space used by the map renderer.""" 133 return self._trace_packet.points if self._trace_packet else [] 134 135 @property 136 def robot_position(self) -> Q10RoborockPoint | None: 137 """Current position in the common Roborock millimetre coordinate space.""" 138 if self._trace_packet is None or (position := self._trace_packet.robot_position) is None: 139 return None 140 return position.to_roborock() 141 142 @property 143 def trace_sequence(self) -> int | None: 144 """Current cleaning-session sequence from the trace stream.""" 145 return self._trace_packet.sequence if self._trace_packet else None 146 147 @property 148 def robot_heading(self) -> int | None: 149 """Current heading for orienting a robot marker on a caller-rendered map.""" 150 return self._trace_packet.heading if self._trace_packet else None 151 152 def update_from_map_packet(self, packet: Q10MapPacket) -> None: 153 """Store a map-protocol update and render the latest sources.""" 154 self._map_packet = packet 155 self._render() 156 self._notify_update() 157 158 def update_from_trace_packet(self, packet: Q10TracePacket) -> None: 159 """Store a trace-protocol update and render the latest sources.""" 160 self._trace_packet = packet 161 self._render() 162 self._notify_update() 163 164 def _map_dps_updated(self) -> None: 165 """Render after the low-level map DPS source changes.""" 166 if self._map_packet is None: 167 return 168 self._render() 169 self._notify_update() 170 171 def _render(self) -> None: 172 """Render the required map with the latest optional trace and overlays.""" 173 if self._map_packet is None: 174 return 175 try: 176 self._image_content = render_q10_map( 177 self._map_packet, 178 self._trace_packet if not self._map_dps.robot_at_dock else None, 179 self._map_dps.overlays, 180 config=self._config, 181 robot_at_dock=self._map_dps.robot_at_dock, 182 ) 183 except RoborockException as ex: 184 _LOGGER.debug("Failed to render Q10 map packet: %s", ex) 185 self._image_content = None 186 187 def as_dict(self, exclude: set[str] | None = None) -> dict[str, Any]: 188 """Return the trait data as a dictionary, excluding large binary data.""" 189 exclude_set = exclude or set() 190 data = { 191 "rooms": [room.as_dict() for room in self.rooms], 192 "path": [point.as_dict() for point in self.path], 193 "robotPosition": ( 194 {"x": position.x, "y": position.y} if (position := self.robot_position) is not None else None 195 ), 196 "robotHeading": self.robot_heading, 197 } 198 for key in exclude_set: 199 data.pop(key, None) 200 return data
High-level composed Q10 map view.
The latest map and trace packets are combined with the injected
MapDpsTrait whenever a source changes. The
MapsTrait supplies a stored ID only when this trait requests
content.
93 def __init__( 94 self, 95 map_dps: MapDpsTrait, 96 maps: MapsTrait, 97 command: CommandTrait, 98 *, 99 map_parser_config: B01Q10MapParserConfig | None = None, 100 ) -> None: 101 TraitUpdateListener.__init__(self, logger=_LOGGER) 102 self._config = map_parser_config or B01Q10MapParserConfig() 103 self._map_dps = map_dps 104 self._maps = maps 105 self._command = command 106 self._map_packet: Q10MapPacket | None = None 107 self._trace_packet: Q10TracePacket | None = None 108 self._image_content: bytes | None = None 109 self._map_dps.add_update_listener(self._map_dps_updated)
Initialize the trait update listener.
111 async def refresh(self) -> None: 112 """Request a safe asynchronous current-map/status push. 113 114 Some ss07 firmware treats ``dpMultiMap op:get`` as an active 115 cleaning/relocation command. ``REQUEST_DPS`` is the device's read-only 116 current-map request and does not depend on a saved-map ID. 117 """ 118 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.
120 @property 121 def image_content(self) -> bytes | None: 122 """The composed map PNG, if the latest map rendered successfully.""" 123 return self._image_content
The composed map PNG, if the latest map rendered successfully.
125 @property 126 def rooms(self) -> list[Q10Room]: 127 """Rooms reported by the device.""" 128 return self._map_packet.rooms if self._map_packet else []
Rooms reported by the device.
130 @property 131 def path(self) -> list[Q10Point]: 132 """Full path in the Q10 trace coordinate space used by the map renderer.""" 133 return self._trace_packet.points if self._trace_packet else []
Full path in the Q10 trace coordinate space used by the map renderer.
135 @property 136 def robot_position(self) -> Q10RoborockPoint | None: 137 """Current position in the common Roborock millimetre coordinate space.""" 138 if self._trace_packet is None or (position := self._trace_packet.robot_position) is None: 139 return None 140 return position.to_roborock()
Current position in the common Roborock millimetre coordinate space.
142 @property 143 def trace_sequence(self) -> int | None: 144 """Current cleaning-session sequence from the trace stream.""" 145 return self._trace_packet.sequence if self._trace_packet else None
Current cleaning-session sequence from the trace stream.
147 @property 148 def robot_heading(self) -> int | None: 149 """Current heading for orienting a robot marker on a caller-rendered map.""" 150 return self._trace_packet.heading if self._trace_packet else None
Current heading for orienting a robot marker on a caller-rendered map.
152 def update_from_map_packet(self, packet: Q10MapPacket) -> None: 153 """Store a map-protocol update and render the latest sources.""" 154 self._map_packet = packet 155 self._render() 156 self._notify_update()
Store a map-protocol update and render the latest sources.
158 def update_from_trace_packet(self, packet: Q10TracePacket) -> None: 159 """Store a trace-protocol update and render the latest sources.""" 160 self._trace_packet = packet 161 self._render() 162 self._notify_update()
Store a trace-protocol update and render the latest sources.
187 def as_dict(self, exclude: set[str] | None = None) -> dict[str, Any]: 188 """Return the trait data as a dictionary, excluding large binary data.""" 189 exclude_set = exclude or set() 190 data = { 191 "rooms": [room.as_dict() for room in self.rooms], 192 "path": [point.as_dict() for point in self.path], 193 "robotPosition": ( 194 {"x": position.x, "y": position.y} if (position := self.robot_position) is not None else None 195 ), 196 "robotHeading": self.robot_heading, 197 } 198 for key in exclude_set: 199 data.pop(key, None) 200 return data
Return the trait data as a dictionary, excluding large binary data.
33class MapsTrait(Maps, UpdatableTrait): 34 """Request and store the Q10 saved-map list.""" 35 36 _CONVERTER = DpsDataConverter.from_dataclass(Maps) 37 _command: CommandTrait 38 39 def __init__(self, command: CommandTrait) -> None: 40 """Initialize the saved-map list trait.""" 41 Maps.__init__(self) 42 UpdatableTrait.__init__(self, command, _LOGGER) 43 self._command = command 44 45 async def refresh(self) -> None: 46 """Request a new saved-map list from the device.""" 47 await self._command.send( 48 B01_Q10_DP.COMMON, 49 {str(B01_Q10_DP.MULTI_MAP.code): {"op": "list"}}, 50 ) 51 52 def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None: 53 """Store a successful saved-map list response.""" 54 response = decoded_dps.get(B01_Q10_DP.MULTI_MAP) 55 # DP 61 also carries map-content acknowledgements. Ignore them so they 56 # cannot replace a usable map list with an unrelated response. 57 if not isinstance(response, dict) or response.get("op") != "list" or response.get("result") != 1: 58 return 59 super().update_from_dps(decoded_dps)
Request and store the Q10 saved-map list.
39 def __init__(self, command: CommandTrait) -> None: 40 """Initialize the saved-map list trait.""" 41 Maps.__init__(self) 42 UpdatableTrait.__init__(self, command, _LOGGER) 43 self._command = command
Initialize the saved-map list trait.
45 async def refresh(self) -> None: 46 """Request a new saved-map list from the device.""" 47 await self._command.send( 48 B01_Q10_DP.COMMON, 49 {str(B01_Q10_DP.MULTI_MAP.code): {"op": "list"}}, 50 )
Request a new saved-map list from the device.
52 def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None: 53 """Store a successful saved-map list response.""" 54 response = decoded_dps.get(B01_Q10_DP.MULTI_MAP) 55 # DP 61 also carries map-content acknowledgements. Ignore them so they 56 # cannot replace a usable map list with an unrelated response. 57 if not isinstance(response, dict) or response.get("op") != "list" or response.get("result") != 1: 58 return 59 super().update_from_dps(decoded_dps)
Store a successful saved-map list response.
Inherited Members
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).
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.
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.
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).
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.
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.
Inherited Members
- roborock.data.b01_q10.b01_q10_containers.Q10Status
- clean_time
- clean_area
- battery
- status
- fan_level
- water_level
- clean_count
- total_clean_area
- total_clean_count
- total_clean_time
- clean_mode
- clean_task_type
- back_type
- cleaning_progress
- fault
- clean_line
- carpet_clean_type
- area_unit
- auto_boost
- multi_map_switch
- map_save_switch
- recent_clean_record
- valley_point_charging
- line_laser_obstacle_avoidance
- mop_state
- ground_clean
- add_clean_state
- robot_country_code
- time_zone
- breakpoint_clean
- timer_type
- user_plan
- robot_type
- main_brush_life
- side_brush_life
- filter_life
- sensor_life
- fault_name