roborock.data.b01_q7.b01_q7_containers
1import datetime 2import json 3from dataclasses import dataclass, field 4from functools import cached_property 5 6from ...exceptions import RoborockException 7from ..containers import RoborockBase 8from .b01_q7_code_mappings import ( 9 B01Fault, 10 CleanPathPreferenceMapping, 11 CleanRepeatMapping, 12 CleanTypeMapping, 13 SCWindMapping, 14 WaterLevelMapping, 15 WorkModeMapping, 16 WorkStatusMapping, 17) 18 19 20@dataclass 21class NetStatus(RoborockBase): 22 """Represents the network status of the device.""" 23 24 rssi: str 25 loss: int 26 ping: int 27 ip: str 28 mac: str 29 ssid: str 30 frequency: int 31 bssid: str 32 33 34@dataclass 35class OrderTotal(RoborockBase): 36 """Represents the order total information.""" 37 38 total: int 39 enable: int 40 41 42@dataclass 43class Privacy(RoborockBase): 44 """Represents the privacy settings of the device.""" 45 46 ai_recognize: int 47 dirt_recognize: int 48 pet_recognize: int 49 carpet_turbo: int 50 carpet_avoid: int 51 carpet_show: int 52 map_uploads: int 53 ai_agent: int 54 ai_avoidance: int 55 record_uploads: int 56 along_floor: int 57 auto_upgrade: int 58 59 60@dataclass 61class PvCharging(RoborockBase): 62 """Represents the photovoltaic charging status.""" 63 64 status: int 65 begin_time: int 66 end_time: int 67 68 69@dataclass 70class Recommend(RoborockBase): 71 """Represents cleaning recommendations.""" 72 73 sill: int 74 wall: int 75 room_id: list[int] = field(default_factory=list) 76 77 78@dataclass 79class Q7MapListEntry(RoborockBase): 80 """Single map list entry returned by `service.get_map_list`.""" 81 82 id: int | None = None 83 cur: bool | None = None 84 85 86@dataclass 87class Q7MapList(RoborockBase): 88 """Map list response returned by `service.get_map_list`.""" 89 90 map_list: list[Q7MapListEntry] = field(default_factory=list) 91 92 @property 93 def current_map_id(self) -> int | None: 94 """Current map id, preferring the entry marked current.""" 95 if not self.map_list: 96 return None 97 98 ordered = sorted(self.map_list, key=lambda entry: entry.cur or False, reverse=True) 99 first = next(iter(ordered), None) 100 if first is None or not isinstance(first.id, int): 101 return None 102 return first.id 103 104 105@dataclass 106class B01Props(RoborockBase): 107 """ 108 Represents the complete properties and status for a Roborock B01 model. 109 This dataclass is generated based on the device's status JSON object. 110 """ 111 112 status: WorkStatusMapping | None = None 113 fault: B01Fault | None = None 114 wind: SCWindMapping | None = None 115 water: WaterLevelMapping | None = None 116 mode: CleanTypeMapping | None = None 117 quantity: int | None = None # The Q7 L5 reports its battery level as 'quantity' 118 alarm: int | None = None 119 volume: int | None = None 120 hypa: int | None = None 121 main_brush: int | None = None 122 side_brush: int | None = None 123 mop_life: int | None = None 124 main_sensor: int | None = None 125 net_status: NetStatus | None = None 126 repeat_state: CleanRepeatMapping | None = None 127 tank_state: int | None = None 128 sweep_type: int | None = None 129 clean_path_preference: CleanPathPreferenceMapping | None = None 130 cloth_state: int | None = None 131 time_zone: int | None = None 132 time_zone_info: str | None = None 133 language: int | None = None 134 cleaning_time: int | None = None 135 real_clean_time: int | None = None 136 cleaning_area: int | None = None 137 custom_type: int | None = None 138 sound: int | None = None 139 work_mode: WorkModeMapping | None = None 140 station_act: int | None = None 141 charge_state: int | None = None 142 current_map_id: int | None = None 143 map_num: int | None = None 144 dust_action: int | None = None 145 quiet_is_open: int | None = None 146 quiet_begin_time: int | None = None 147 quiet_end_time: int | None = None 148 clean_finish: int | None = None 149 voice_type: int | None = None 150 voice_type_version: int | None = None 151 order_total: OrderTotal | None = None 152 build_map: int | None = None 153 privacy: Privacy | None = None 154 dust_auto_state: int | None = None 155 dust_frequency: int | None = None 156 child_lock: int | None = None 157 multi_floor: int | None = None 158 map_save: int | None = None 159 light_mode: int | None = None 160 green_laser: int | None = None 161 dust_bag_used: int | None = None 162 order_save_mode: int | None = None 163 manufacturer: str | None = None 164 back_to_wash: int | None = None 165 charge_station_type: int | None = None 166 pv_cut_charge: int | None = None 167 pv_charging: PvCharging | None = None 168 serial_number: str | None = None 169 recommend: Recommend | None = None 170 add_sweep_status: int | None = None 171 172 @property 173 def battery(self) -> int | None: 174 """ 175 Returns device battery level as a percentage. 176 """ 177 return self.quantity 178 179 @property 180 def main_brush_time_left(self) -> int | None: 181 """ 182 Returns estimated remaining life of the main brush in minutes. 183 Total life is 300 hours (18000 minutes). 184 """ 185 if self.main_brush is None: 186 return None 187 return max(0, 18000 - self.main_brush) 188 189 @property 190 def side_brush_time_left(self) -> int | None: 191 """ 192 Returns estimated remaining life of the side brush in minutes. 193 Total life is 200 hours (12000 minutes). 194 """ 195 if self.side_brush is None: 196 return None 197 return max(0, 12000 - self.side_brush) 198 199 @property 200 def filter_time_left(self) -> int | None: 201 """ 202 Returns estimated remaining life of the filter (hypa) in minutes. 203 Total life is 150 hours (9000 minutes). 204 """ 205 if self.hypa is None: 206 return None 207 return max(0, 9000 - self.hypa) 208 209 @property 210 def mop_life_time_left(self) -> int | None: 211 """ 212 Returns estimated remaining life of the mop in minutes. 213 Total life is 180 hours (10800 minutes). 214 """ 215 if self.mop_life is None: 216 return None 217 return max(0, 10800 - self.mop_life) 218 219 @property 220 def sensor_dirty_time_left(self) -> int | None: 221 """ 222 Returns estimated time until sensors need cleaning in minutes. 223 Maintenance interval is typically 30 hours (1800 minutes). 224 """ 225 if self.main_sensor is None: 226 return None 227 return max(0, 1800 - self.main_sensor) 228 229 @property 230 def status_name(self) -> str | None: 231 """Returns the name of the current status.""" 232 return self.status.value if self.status is not None else None 233 234 @property 235 def fault_name(self) -> str | None: 236 """Returns the name of the current fault.""" 237 return self.fault.value if self.fault is not None else None 238 239 @property 240 def wind_name(self) -> str | None: 241 """Returns the name of the current fan speed (wind).""" 242 return self.wind.value if self.wind is not None else None 243 244 @property 245 def work_mode_name(self) -> str | None: 246 """Returns the name of the current work mode.""" 247 return self.work_mode.value if self.work_mode is not None else None 248 249 @property 250 def repeat_state_name(self) -> str | None: 251 """Returns the name of the current repeat state.""" 252 return self.repeat_state.value if self.repeat_state is not None else None 253 254 @property 255 def clean_path_preference_name(self) -> str | None: 256 """Returns the name of the current clean path preference.""" 257 return self.clean_path_preference.value if self.clean_path_preference is not None else None 258 259 260@dataclass 261class CleanRecordDetail(RoborockBase): 262 """Represents a single clean record detail (from `record_list[].detail`).""" 263 264 record_start_time: int | None = None 265 method: int | None = None 266 record_use_time: int | None = None 267 clean_count: int | None = None 268 # This is seemingly returned in meters (non-squared) 269 record_clean_area: int | None = None 270 record_clean_mode: int | None = None 271 record_clean_way: int | None = None 272 record_task_status: int | None = None 273 record_faultcode: int | None = None 274 record_dust_num: int | None = None 275 clean_current_map: int | None = None 276 record_map_url: str | None = None 277 278 @property 279 def start_datetime(self) -> datetime.datetime | None: 280 """Convert the start datetime into a datetime object.""" 281 if self.record_start_time is not None: 282 return datetime.datetime.fromtimestamp(self.record_start_time).astimezone(datetime.UTC) 283 return None 284 285 @property 286 def square_meters_area_cleaned(self) -> float | None: 287 """Returns the area cleaned in square meters.""" 288 if self.record_clean_area is not None: 289 return self.record_clean_area / 100 290 return None 291 292 293@dataclass 294class CleanRecordListItem(RoborockBase): 295 """Represents an entry in the clean record list returned by `service.get_record_list`.""" 296 297 url: str | None = None 298 detail: str | None = None 299 300 @cached_property 301 def detail_parsed(self) -> CleanRecordDetail | None: 302 """Parse and return the detail as a CleanRecordDetail object.""" 303 if self.detail is None: 304 return None 305 try: 306 parsed = json.loads(self.detail) 307 except json.JSONDecodeError as ex: 308 raise RoborockException(f"Invalid B01 record detail JSON: {self.detail!r}") from ex 309 return CleanRecordDetail.from_dict(parsed) 310 311 312@dataclass 313class CleanRecordList(RoborockBase): 314 """Represents the clean record list response from `service.get_record_list`.""" 315 316 total_area: int | None = None 317 total_time: int | None = None # stored in seconds 318 total_count: int | None = None 319 record_list: list[CleanRecordListItem] = field(default_factory=list) 320 321 @property 322 def square_meters_area_cleaned(self) -> float | None: 323 """Returns the area cleaned in square meters.""" 324 if self.total_area is not None: 325 return self.total_area / 100 326 return None 327 328 329@dataclass 330class CleanRecordSummary(RoborockBase): 331 """Represents clean record totals for B01/Q7 devices.""" 332 333 total_time: int | None = None 334 total_area: int | None = None 335 total_count: int | None = None 336 last_record_detail: CleanRecordDetail | None = None
21@dataclass 22class NetStatus(RoborockBase): 23 """Represents the network status of the device.""" 24 25 rssi: str 26 loss: int 27 ping: int 28 ip: str 29 mac: str 30 ssid: str 31 frequency: int 32 bssid: str
Represents the network status of the device.
Inherited Members
35@dataclass 36class OrderTotal(RoborockBase): 37 """Represents the order total information.""" 38 39 total: int 40 enable: int
Represents the order total information.
Inherited Members
43@dataclass 44class Privacy(RoborockBase): 45 """Represents the privacy settings of the device.""" 46 47 ai_recognize: int 48 dirt_recognize: int 49 pet_recognize: int 50 carpet_turbo: int 51 carpet_avoid: int 52 carpet_show: int 53 map_uploads: int 54 ai_agent: int 55 ai_avoidance: int 56 record_uploads: int 57 along_floor: int 58 auto_upgrade: int
Represents the privacy settings of the device.
Inherited Members
61@dataclass 62class PvCharging(RoborockBase): 63 """Represents the photovoltaic charging status.""" 64 65 status: int 66 begin_time: int 67 end_time: int
Represents the photovoltaic charging status.
Inherited Members
70@dataclass 71class Recommend(RoborockBase): 72 """Represents cleaning recommendations.""" 73 74 sill: int 75 wall: int 76 room_id: list[int] = field(default_factory=list)
Represents cleaning recommendations.
Inherited Members
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.
Inherited Members
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.
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.
Inherited Members
106@dataclass 107class B01Props(RoborockBase): 108 """ 109 Represents the complete properties and status for a Roborock B01 model. 110 This dataclass is generated based on the device's status JSON object. 111 """ 112 113 status: WorkStatusMapping | None = None 114 fault: B01Fault | None = None 115 wind: SCWindMapping | None = None 116 water: WaterLevelMapping | None = None 117 mode: CleanTypeMapping | None = None 118 quantity: int | None = None # The Q7 L5 reports its battery level as 'quantity' 119 alarm: int | None = None 120 volume: int | None = None 121 hypa: int | None = None 122 main_brush: int | None = None 123 side_brush: int | None = None 124 mop_life: int | None = None 125 main_sensor: int | None = None 126 net_status: NetStatus | None = None 127 repeat_state: CleanRepeatMapping | None = None 128 tank_state: int | None = None 129 sweep_type: int | None = None 130 clean_path_preference: CleanPathPreferenceMapping | None = None 131 cloth_state: int | None = None 132 time_zone: int | None = None 133 time_zone_info: str | None = None 134 language: int | None = None 135 cleaning_time: int | None = None 136 real_clean_time: int | None = None 137 cleaning_area: int | None = None 138 custom_type: int | None = None 139 sound: int | None = None 140 work_mode: WorkModeMapping | None = None 141 station_act: int | None = None 142 charge_state: int | None = None 143 current_map_id: int | None = None 144 map_num: int | None = None 145 dust_action: int | None = None 146 quiet_is_open: int | None = None 147 quiet_begin_time: int | None = None 148 quiet_end_time: int | None = None 149 clean_finish: int | None = None 150 voice_type: int | None = None 151 voice_type_version: int | None = None 152 order_total: OrderTotal | None = None 153 build_map: int | None = None 154 privacy: Privacy | None = None 155 dust_auto_state: int | None = None 156 dust_frequency: int | None = None 157 child_lock: int | None = None 158 multi_floor: int | None = None 159 map_save: int | None = None 160 light_mode: int | None = None 161 green_laser: int | None = None 162 dust_bag_used: int | None = None 163 order_save_mode: int | None = None 164 manufacturer: str | None = None 165 back_to_wash: int | None = None 166 charge_station_type: int | None = None 167 pv_cut_charge: int | None = None 168 pv_charging: PvCharging | None = None 169 serial_number: str | None = None 170 recommend: Recommend | None = None 171 add_sweep_status: int | None = None 172 173 @property 174 def battery(self) -> int | None: 175 """ 176 Returns device battery level as a percentage. 177 """ 178 return self.quantity 179 180 @property 181 def main_brush_time_left(self) -> int | None: 182 """ 183 Returns estimated remaining life of the main brush in minutes. 184 Total life is 300 hours (18000 minutes). 185 """ 186 if self.main_brush is None: 187 return None 188 return max(0, 18000 - self.main_brush) 189 190 @property 191 def side_brush_time_left(self) -> int | None: 192 """ 193 Returns estimated remaining life of the side brush in minutes. 194 Total life is 200 hours (12000 minutes). 195 """ 196 if self.side_brush is None: 197 return None 198 return max(0, 12000 - self.side_brush) 199 200 @property 201 def filter_time_left(self) -> int | None: 202 """ 203 Returns estimated remaining life of the filter (hypa) in minutes. 204 Total life is 150 hours (9000 minutes). 205 """ 206 if self.hypa is None: 207 return None 208 return max(0, 9000 - self.hypa) 209 210 @property 211 def mop_life_time_left(self) -> int | None: 212 """ 213 Returns estimated remaining life of the mop in minutes. 214 Total life is 180 hours (10800 minutes). 215 """ 216 if self.mop_life is None: 217 return None 218 return max(0, 10800 - self.mop_life) 219 220 @property 221 def sensor_dirty_time_left(self) -> int | None: 222 """ 223 Returns estimated time until sensors need cleaning in minutes. 224 Maintenance interval is typically 30 hours (1800 minutes). 225 """ 226 if self.main_sensor is None: 227 return None 228 return max(0, 1800 - self.main_sensor) 229 230 @property 231 def status_name(self) -> str | None: 232 """Returns the name of the current status.""" 233 return self.status.value if self.status is not None else None 234 235 @property 236 def fault_name(self) -> str | None: 237 """Returns the name of the current fault.""" 238 return self.fault.value if self.fault is not None else None 239 240 @property 241 def wind_name(self) -> str | None: 242 """Returns the name of the current fan speed (wind).""" 243 return self.wind.value if self.wind is not None else None 244 245 @property 246 def work_mode_name(self) -> str | None: 247 """Returns the name of the current work mode.""" 248 return self.work_mode.value if self.work_mode is not None else None 249 250 @property 251 def repeat_state_name(self) -> str | None: 252 """Returns the name of the current repeat state.""" 253 return self.repeat_state.value if self.repeat_state is not None else None 254 255 @property 256 def clean_path_preference_name(self) -> str | None: 257 """Returns the name of the current clean path preference.""" 258 return self.clean_path_preference.value if self.clean_path_preference is not None else None
Represents the complete properties and status for a Roborock B01 model. This dataclass is generated based on the device's status JSON object.
173 @property 174 def battery(self) -> int | None: 175 """ 176 Returns device battery level as a percentage. 177 """ 178 return self.quantity
Returns device battery level as a percentage.
180 @property 181 def main_brush_time_left(self) -> int | None: 182 """ 183 Returns estimated remaining life of the main brush in minutes. 184 Total life is 300 hours (18000 minutes). 185 """ 186 if self.main_brush is None: 187 return None 188 return max(0, 18000 - self.main_brush)
Returns estimated remaining life of the main brush in minutes. Total life is 300 hours (18000 minutes).
190 @property 191 def side_brush_time_left(self) -> int | None: 192 """ 193 Returns estimated remaining life of the side brush in minutes. 194 Total life is 200 hours (12000 minutes). 195 """ 196 if self.side_brush is None: 197 return None 198 return max(0, 12000 - self.side_brush)
Returns estimated remaining life of the side brush in minutes. Total life is 200 hours (12000 minutes).
200 @property 201 def filter_time_left(self) -> int | None: 202 """ 203 Returns estimated remaining life of the filter (hypa) in minutes. 204 Total life is 150 hours (9000 minutes). 205 """ 206 if self.hypa is None: 207 return None 208 return max(0, 9000 - self.hypa)
Returns estimated remaining life of the filter (hypa) in minutes. Total life is 150 hours (9000 minutes).
210 @property 211 def mop_life_time_left(self) -> int | None: 212 """ 213 Returns estimated remaining life of the mop in minutes. 214 Total life is 180 hours (10800 minutes). 215 """ 216 if self.mop_life is None: 217 return None 218 return max(0, 10800 - self.mop_life)
Returns estimated remaining life of the mop in minutes. Total life is 180 hours (10800 minutes).
220 @property 221 def sensor_dirty_time_left(self) -> int | None: 222 """ 223 Returns estimated time until sensors need cleaning in minutes. 224 Maintenance interval is typically 30 hours (1800 minutes). 225 """ 226 if self.main_sensor is None: 227 return None 228 return max(0, 1800 - self.main_sensor)
Returns estimated time until sensors need cleaning in minutes. Maintenance interval is typically 30 hours (1800 minutes).
230 @property 231 def status_name(self) -> str | None: 232 """Returns the name of the current status.""" 233 return self.status.value if self.status is not None else None
Returns the name of the current status.
235 @property 236 def fault_name(self) -> str | None: 237 """Returns the name of the current fault.""" 238 return self.fault.value if self.fault is not None else None
Returns the name of the current fault.
240 @property 241 def wind_name(self) -> str | None: 242 """Returns the name of the current fan speed (wind).""" 243 return self.wind.value if self.wind is not None else None
Returns the name of the current fan speed (wind).
245 @property 246 def work_mode_name(self) -> str | None: 247 """Returns the name of the current work mode.""" 248 return self.work_mode.value if self.work_mode is not None else None
Returns the name of the current work mode.
250 @property 251 def repeat_state_name(self) -> str | None: 252 """Returns the name of the current repeat state.""" 253 return self.repeat_state.value if self.repeat_state is not None else None
Returns the name of the current repeat state.
255 @property 256 def clean_path_preference_name(self) -> str | None: 257 """Returns the name of the current clean path preference.""" 258 return self.clean_path_preference.value if self.clean_path_preference is not None else None
Returns the name of the current clean path preference.
Inherited Members
261@dataclass 262class CleanRecordDetail(RoborockBase): 263 """Represents a single clean record detail (from `record_list[].detail`).""" 264 265 record_start_time: int | None = None 266 method: int | None = None 267 record_use_time: int | None = None 268 clean_count: int | None = None 269 # This is seemingly returned in meters (non-squared) 270 record_clean_area: int | None = None 271 record_clean_mode: int | None = None 272 record_clean_way: int | None = None 273 record_task_status: int | None = None 274 record_faultcode: int | None = None 275 record_dust_num: int | None = None 276 clean_current_map: int | None = None 277 record_map_url: str | None = None 278 279 @property 280 def start_datetime(self) -> datetime.datetime | None: 281 """Convert the start datetime into a datetime object.""" 282 if self.record_start_time is not None: 283 return datetime.datetime.fromtimestamp(self.record_start_time).astimezone(datetime.UTC) 284 return None 285 286 @property 287 def square_meters_area_cleaned(self) -> float | None: 288 """Returns the area cleaned in square meters.""" 289 if self.record_clean_area is not None: 290 return self.record_clean_area / 100 291 return None
Represents a single clean record detail (from record_list[].detail).
279 @property 280 def start_datetime(self) -> datetime.datetime | None: 281 """Convert the start datetime into a datetime object.""" 282 if self.record_start_time is not None: 283 return datetime.datetime.fromtimestamp(self.record_start_time).astimezone(datetime.UTC) 284 return None
Convert the start datetime into a datetime object.
286 @property 287 def square_meters_area_cleaned(self) -> float | None: 288 """Returns the area cleaned in square meters.""" 289 if self.record_clean_area is not None: 290 return self.record_clean_area / 100 291 return None
Returns the area cleaned in square meters.
Inherited Members
294@dataclass 295class CleanRecordListItem(RoborockBase): 296 """Represents an entry in the clean record list returned by `service.get_record_list`.""" 297 298 url: str | None = None 299 detail: str | None = None 300 301 @cached_property 302 def detail_parsed(self) -> CleanRecordDetail | None: 303 """Parse and return the detail as a CleanRecordDetail object.""" 304 if self.detail is None: 305 return None 306 try: 307 parsed = json.loads(self.detail) 308 except json.JSONDecodeError as ex: 309 raise RoborockException(f"Invalid B01 record detail JSON: {self.detail!r}") from ex 310 return CleanRecordDetail.from_dict(parsed)
Represents an entry in the clean record list returned by service.get_record_list.
301 @cached_property 302 def detail_parsed(self) -> CleanRecordDetail | None: 303 """Parse and return the detail as a CleanRecordDetail object.""" 304 if self.detail is None: 305 return None 306 try: 307 parsed = json.loads(self.detail) 308 except json.JSONDecodeError as ex: 309 raise RoborockException(f"Invalid B01 record detail JSON: {self.detail!r}") from ex 310 return CleanRecordDetail.from_dict(parsed)
Parse and return the detail as a CleanRecordDetail object.
Inherited Members
313@dataclass 314class CleanRecordList(RoborockBase): 315 """Represents the clean record list response from `service.get_record_list`.""" 316 317 total_area: int | None = None 318 total_time: int | None = None # stored in seconds 319 total_count: int | None = None 320 record_list: list[CleanRecordListItem] = field(default_factory=list) 321 322 @property 323 def square_meters_area_cleaned(self) -> float | None: 324 """Returns the area cleaned in square meters.""" 325 if self.total_area is not None: 326 return self.total_area / 100 327 return None
Represents the clean record list response from service.get_record_list.
322 @property 323 def square_meters_area_cleaned(self) -> float | None: 324 """Returns the area cleaned in square meters.""" 325 if self.total_area is not None: 326 return self.total_area / 100 327 return None
Returns the area cleaned in square meters.
Inherited Members
330@dataclass 331class CleanRecordSummary(RoborockBase): 332 """Represents clean record totals for B01/Q7 devices.""" 333 334 total_time: int | None = None 335 total_area: int | None = None 336 total_count: int | None = None 337 last_record_detail: CleanRecordDetail | None = None
Represents clean record totals for B01/Q7 devices.