roborock.devices.traits.b01.q7

Traits for Q7 B01 devices. Potentially other devices may fall into this category in the future.

  1"""Traits for Q7 B01 devices.
  2Potentially other devices may fall into this category in the future."""
  3
  4from typing import Any
  5
  6from roborock import B01Props
  7from roborock.data import Q7MapList, Q7MapListEntry
  8from roborock.data.b01_q7.b01_q7_code_mappings import (
  9    CleanPathPreferenceMapping,
 10    CleanRepeatMapping,
 11    CleanTaskTypeMapping,
 12    CleanTypeMapping,
 13    SCDeviceCleanParam,
 14    SCWindMapping,
 15    WaterLevelMapping,
 16)
 17from roborock.devices.rpc.b01_q7_channel import send_decoded_command
 18from roborock.devices.traits import Trait
 19from roborock.devices.transport.mqtt_channel import MqttChannel
 20from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, CommandType, ParamsType, Q7RequestMessage
 21from roborock.roborock_message import RoborockB01Props
 22from roborock.roborock_typing import RoborockB01Q7Methods
 23
 24from .clean_summary import CleanSummaryTrait
 25from .map import MapTrait
 26
 27__all__ = [
 28    "Q7PropertiesApi",
 29    "CleanSummaryTrait",
 30    "MapTrait",
 31    "Q7MapList",
 32    "Q7MapListEntry",
 33]
 34
 35
 36class Q7PropertiesApi(Trait):
 37    """API for interacting with B01 devices."""
 38
 39    clean_summary: CleanSummaryTrait
 40    """Trait for clean records / clean summary (Q7 `service.get_record_list`)."""
 41
 42    map: MapTrait
 43    """Trait for map list metadata + raw map payload retrieval."""
 44
 45    def __init__(self, channel: MqttChannel) -> None:
 46        """Initialize the B01Props API."""
 47        self._channel = channel
 48        self.clean_summary = CleanSummaryTrait(channel)
 49        self.map = MapTrait(channel)
 50
 51    async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
 52        """Query the device for the values of the given Q7 properties."""
 53        result = await self.send(
 54            RoborockB01Q7Methods.GET_PROP,
 55            {"property": props},
 56        )
 57        if not isinstance(result, dict):
 58            raise TypeError(f"Unexpected response type for GET_PROP: {type(result).__name__}: {result!r}")
 59        return B01Props.from_dict(result)
 60
 61    async def set_prop(self, prop: RoborockB01Props, value: Any) -> None:
 62        """Set a property on the device."""
 63        await self.send(
 64            command=RoborockB01Q7Methods.SET_PROP,
 65            params={prop: value},
 66        )
 67
 68    async def set_fan_speed(self, fan_speed: SCWindMapping) -> None:
 69        """Set the fan speed (wind)."""
 70        await self.set_prop(RoborockB01Props.WIND, fan_speed.code)
 71
 72    async def set_water_level(self, water_level: WaterLevelMapping) -> None:
 73        """Set the water level (water)."""
 74        await self.set_prop(RoborockB01Props.WATER, water_level.code)
 75
 76    async def set_mode(self, mode: CleanTypeMapping) -> None:
 77        """Set the cleaning mode (vacuum, mop, or vacuum and mop)."""
 78        await self.set_prop(RoborockB01Props.MODE, mode.code)
 79
 80    async def set_clean_path_preference(self, preference: CleanPathPreferenceMapping) -> None:
 81        """Set the cleaning path preference (route)."""
 82        await self.set_prop(RoborockB01Props.CLEAN_PATH_PREFERENCE, preference.code)
 83
 84    async def set_repeat_state(self, repeat: CleanRepeatMapping) -> None:
 85        """Set the cleaning repeat state (cycles)."""
 86        await self.set_prop(RoborockB01Props.REPEAT_STATE, repeat.code)
 87
 88    async def start_clean(self) -> None:
 89        """Start cleaning."""
 90        await self.send(
 91            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
 92            params={
 93                "clean_type": CleanTaskTypeMapping.ALL.code,
 94                "ctrl_value": SCDeviceCleanParam.START.code,
 95                "room_ids": [],
 96            },
 97        )
 98
 99    async def clean_segments(self, segment_ids: list[int]) -> None:
100        """Start segment cleaning for the given ids (Q7 uses room ids)."""
101        await self.send(
102            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
103            params={
104                "clean_type": CleanTaskTypeMapping.ROOM.code,
105                "ctrl_value": SCDeviceCleanParam.START.code,
106                "room_ids": segment_ids,
107            },
108        )
109
110    async def pause_clean(self) -> None:
111        """Pause cleaning."""
112        await self.send(
113            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
114            params={
115                "clean_type": CleanTaskTypeMapping.ALL.code,
116                "ctrl_value": SCDeviceCleanParam.PAUSE.code,
117                "room_ids": [],
118            },
119        )
120
121    async def stop_clean(self) -> None:
122        """Stop cleaning."""
123        await self.send(
124            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
125            params={
126                "clean_type": CleanTaskTypeMapping.ALL.code,
127                "ctrl_value": SCDeviceCleanParam.STOP.code,
128                "room_ids": [],
129            },
130        )
131
132    async def return_to_dock(self) -> None:
133        """Return to dock."""
134        await self.send(
135            command=RoborockB01Q7Methods.START_RECHARGE,
136            params={},
137        )
138
139    async def find_me(self) -> None:
140        """Locate the robot."""
141        await self.send(
142            command=RoborockB01Q7Methods.FIND_DEVICE,
143            params={},
144        )
145
146    async def send(self, command: CommandType, params: ParamsType) -> Any:
147        """Send a command to the device."""
148        return await send_decoded_command(
149            self._channel,
150            Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params),
151        )
152
153
154def create(channel: MqttChannel) -> Q7PropertiesApi:
155    """Create traits for B01 devices."""
156    return Q7PropertiesApi(channel)
class Q7PropertiesApi(roborock.devices.traits.Trait):
 37class Q7PropertiesApi(Trait):
 38    """API for interacting with B01 devices."""
 39
 40    clean_summary: CleanSummaryTrait
 41    """Trait for clean records / clean summary (Q7 `service.get_record_list`)."""
 42
 43    map: MapTrait
 44    """Trait for map list metadata + raw map payload retrieval."""
 45
 46    def __init__(self, channel: MqttChannel) -> None:
 47        """Initialize the B01Props API."""
 48        self._channel = channel
 49        self.clean_summary = CleanSummaryTrait(channel)
 50        self.map = MapTrait(channel)
 51
 52    async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
 53        """Query the device for the values of the given Q7 properties."""
 54        result = await self.send(
 55            RoborockB01Q7Methods.GET_PROP,
 56            {"property": props},
 57        )
 58        if not isinstance(result, dict):
 59            raise TypeError(f"Unexpected response type for GET_PROP: {type(result).__name__}: {result!r}")
 60        return B01Props.from_dict(result)
 61
 62    async def set_prop(self, prop: RoborockB01Props, value: Any) -> None:
 63        """Set a property on the device."""
 64        await self.send(
 65            command=RoborockB01Q7Methods.SET_PROP,
 66            params={prop: value},
 67        )
 68
 69    async def set_fan_speed(self, fan_speed: SCWindMapping) -> None:
 70        """Set the fan speed (wind)."""
 71        await self.set_prop(RoborockB01Props.WIND, fan_speed.code)
 72
 73    async def set_water_level(self, water_level: WaterLevelMapping) -> None:
 74        """Set the water level (water)."""
 75        await self.set_prop(RoborockB01Props.WATER, water_level.code)
 76
 77    async def set_mode(self, mode: CleanTypeMapping) -> None:
 78        """Set the cleaning mode (vacuum, mop, or vacuum and mop)."""
 79        await self.set_prop(RoborockB01Props.MODE, mode.code)
 80
 81    async def set_clean_path_preference(self, preference: CleanPathPreferenceMapping) -> None:
 82        """Set the cleaning path preference (route)."""
 83        await self.set_prop(RoborockB01Props.CLEAN_PATH_PREFERENCE, preference.code)
 84
 85    async def set_repeat_state(self, repeat: CleanRepeatMapping) -> None:
 86        """Set the cleaning repeat state (cycles)."""
 87        await self.set_prop(RoborockB01Props.REPEAT_STATE, repeat.code)
 88
 89    async def start_clean(self) -> None:
 90        """Start cleaning."""
 91        await self.send(
 92            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
 93            params={
 94                "clean_type": CleanTaskTypeMapping.ALL.code,
 95                "ctrl_value": SCDeviceCleanParam.START.code,
 96                "room_ids": [],
 97            },
 98        )
 99
100    async def clean_segments(self, segment_ids: list[int]) -> None:
101        """Start segment cleaning for the given ids (Q7 uses room ids)."""
102        await self.send(
103            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
104            params={
105                "clean_type": CleanTaskTypeMapping.ROOM.code,
106                "ctrl_value": SCDeviceCleanParam.START.code,
107                "room_ids": segment_ids,
108            },
109        )
110
111    async def pause_clean(self) -> None:
112        """Pause cleaning."""
113        await self.send(
114            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
115            params={
116                "clean_type": CleanTaskTypeMapping.ALL.code,
117                "ctrl_value": SCDeviceCleanParam.PAUSE.code,
118                "room_ids": [],
119            },
120        )
121
122    async def stop_clean(self) -> None:
123        """Stop cleaning."""
124        await self.send(
125            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
126            params={
127                "clean_type": CleanTaskTypeMapping.ALL.code,
128                "ctrl_value": SCDeviceCleanParam.STOP.code,
129                "room_ids": [],
130            },
131        )
132
133    async def return_to_dock(self) -> None:
134        """Return to dock."""
135        await self.send(
136            command=RoborockB01Q7Methods.START_RECHARGE,
137            params={},
138        )
139
140    async def find_me(self) -> None:
141        """Locate the robot."""
142        await self.send(
143            command=RoborockB01Q7Methods.FIND_DEVICE,
144            params={},
145        )
146
147    async def send(self, command: CommandType, params: ParamsType) -> Any:
148        """Send a command to the device."""
149        return await send_decoded_command(
150            self._channel,
151            Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params),
152        )

API for interacting with B01 devices.

Q7PropertiesApi(channel: roborock.devices.transport.mqtt_channel.MqttChannel)
46    def __init__(self, channel: MqttChannel) -> None:
47        """Initialize the B01Props API."""
48        self._channel = channel
49        self.clean_summary = CleanSummaryTrait(channel)
50        self.map = MapTrait(channel)

Initialize the B01Props API.

clean_summary: CleanSummaryTrait

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

map: MapTrait

Trait for map list metadata + raw map payload retrieval.

async def query_values( self, props: list[roborock.roborock_message.RoborockB01Props]) -> roborock.data.b01_q7.b01_q7_containers.B01Props | None:
52    async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
53        """Query the device for the values of the given Q7 properties."""
54        result = await self.send(
55            RoborockB01Q7Methods.GET_PROP,
56            {"property": props},
57        )
58        if not isinstance(result, dict):
59            raise TypeError(f"Unexpected response type for GET_PROP: {type(result).__name__}: {result!r}")
60        return B01Props.from_dict(result)

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

async def set_prop( self, prop: roborock.roborock_message.RoborockB01Props, value: Any) -> None:
62    async def set_prop(self, prop: RoborockB01Props, value: Any) -> None:
63        """Set a property on the device."""
64        await self.send(
65            command=RoborockB01Q7Methods.SET_PROP,
66            params={prop: value},
67        )

Set a property on the device.

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

Set the fan speed (wind).

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

Set the water level (water).

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

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

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

Set the cleaning path preference (route).

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

Set the cleaning repeat state (cycles).

async def start_clean(self) -> None:
89    async def start_clean(self) -> None:
90        """Start cleaning."""
91        await self.send(
92            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
93            params={
94                "clean_type": CleanTaskTypeMapping.ALL.code,
95                "ctrl_value": SCDeviceCleanParam.START.code,
96                "room_ids": [],
97            },
98        )

Start cleaning.

async def clean_segments(self, segment_ids: list[int]) -> None:
100    async def clean_segments(self, segment_ids: list[int]) -> None:
101        """Start segment cleaning for the given ids (Q7 uses room ids)."""
102        await self.send(
103            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
104            params={
105                "clean_type": CleanTaskTypeMapping.ROOM.code,
106                "ctrl_value": SCDeviceCleanParam.START.code,
107                "room_ids": segment_ids,
108            },
109        )

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

async def pause_clean(self) -> None:
111    async def pause_clean(self) -> None:
112        """Pause cleaning."""
113        await self.send(
114            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
115            params={
116                "clean_type": CleanTaskTypeMapping.ALL.code,
117                "ctrl_value": SCDeviceCleanParam.PAUSE.code,
118                "room_ids": [],
119            },
120        )

Pause cleaning.

async def stop_clean(self) -> None:
122    async def stop_clean(self) -> None:
123        """Stop cleaning."""
124        await self.send(
125            command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
126            params={
127                "clean_type": CleanTaskTypeMapping.ALL.code,
128                "ctrl_value": SCDeviceCleanParam.STOP.code,
129                "room_ids": [],
130            },
131        )

Stop cleaning.

async def return_to_dock(self) -> None:
133    async def return_to_dock(self) -> None:
134        """Return to dock."""
135        await self.send(
136            command=RoborockB01Q7Methods.START_RECHARGE,
137            params={},
138        )

Return to dock.

async def find_me(self) -> None:
140    async def find_me(self) -> None:
141        """Locate the robot."""
142        await self.send(
143            command=RoborockB01Q7Methods.FIND_DEVICE,
144            params={},
145        )

Locate the robot.

async def send( self, command: roborock.roborock_typing.RoborockB01Q7Methods | str, params: list | dict | int | None) -> Any:
147    async def send(self, command: CommandType, params: ParamsType) -> Any:
148        """Send a command to the device."""
149        return await send_decoded_command(
150            self._channel,
151            Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params),
152        )

Send a command to the device.

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

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

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

Initialize the clean summary trait.

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

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

Refresh totals and last record detail from the device.

15class MapTrait(Q7MapList, Trait):
16    """Map retrieval + map metadata helpers for Q7 devices."""
17
18    def __init__(self, channel: MqttChannel) -> None:
19        super().__init__()
20        self._channel = channel
21        # Map uploads are serialized per-device to avoid response cross-wiring.
22        self._map_command_lock = asyncio.Lock()
23        self._loaded = False
24
25    async def refresh(self) -> None:
26        """Refresh cached map list metadata from the device."""
27        response = await send_decoded_command(
28            self._channel,
29            Q7RequestMessage(dps=B01_Q7_DPS, command=RoborockB01Q7Methods.GET_MAP_LIST, params={}),
30        )
31        if not isinstance(response, dict):
32            raise RoborockException(
33                f"Unexpected response type for GET_MAP_LIST: {type(response).__name__}: {response!r}"
34            )
35
36        if (parsed := Q7MapList.from_dict(response)) is None:
37            raise RoborockException(f"Failed to decode map list response: {response!r}")
38
39        self.map_list = parsed.map_list
40        self._loaded = True
41
42    async def _get_map_payload(self, *, map_id: int) -> bytes:
43        """Fetch raw map payload bytes for the given map id."""
44        request = Q7RequestMessage(
45            dps=B01_Q7_DPS,
46            command=RoborockB01Q7Methods.UPLOAD_BY_MAPID,
47            params={"map_id": map_id},
48        )
49        async with self._map_command_lock:
50            return await send_map_command(self._channel, request)
51
52    async def get_current_map_payload(self) -> bytes:
53        """Fetch raw map payload bytes for the currently selected map."""
54        if not self._loaded:
55            await self.refresh()
56
57        map_id = self.current_map_id
58        if map_id is None:
59            raise RoborockException(f"Unable to determine map_id from map list response: {self!r}")
60        return await self._get_map_payload(map_id=map_id)

Map retrieval + map metadata helpers for Q7 devices.

MapTrait(channel: roborock.devices.transport.mqtt_channel.MqttChannel)
18    def __init__(self, channel: MqttChannel) -> None:
19        super().__init__()
20        self._channel = channel
21        # Map uploads are serialized per-device to avoid response cross-wiring.
22        self._map_command_lock = asyncio.Lock()
23        self._loaded = False
async def refresh(self) -> None:
25    async def refresh(self) -> None:
26        """Refresh cached map list metadata from the device."""
27        response = await send_decoded_command(
28            self._channel,
29            Q7RequestMessage(dps=B01_Q7_DPS, command=RoborockB01Q7Methods.GET_MAP_LIST, params={}),
30        )
31        if not isinstance(response, dict):
32            raise RoborockException(
33                f"Unexpected response type for GET_MAP_LIST: {type(response).__name__}: {response!r}"
34            )
35
36        if (parsed := Q7MapList.from_dict(response)) is None:
37            raise RoborockException(f"Failed to decode map list response: {response!r}")
38
39        self.map_list = parsed.map_list
40        self._loaded = True

Refresh cached map list metadata from the device.

async def get_current_map_payload(self) -> bytes:
52    async def get_current_map_payload(self) -> bytes:
53        """Fetch raw map payload bytes for the currently selected map."""
54        if not self._loaded:
55            await self.refresh()
56
57        map_id = self.current_map_id
58        if map_id is None:
59            raise RoborockException(f"Unable to determine map_id from map list response: {self!r}")
60        return await self._get_map_payload(map_id=map_id)

Fetch raw map payload bytes for the currently selected map.

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

Map list response returned by service.get_map_list.

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

Current map id, preferring the entry marked current.

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

Single map list entry returned by service.get_map_list.

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